Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3711c2609 | ||
|
|
957fe4637e | ||
|
|
4e9bca25ef | ||
|
|
23d62ab5e6 | ||
|
|
bedefd7e2e | ||
|
|
56688eb1a6 | ||
|
|
ee0049722f | ||
|
|
74934d4559 | ||
|
|
027f88da6c | ||
|
|
175964b652 | ||
|
|
eddb1b24d6 | ||
|
|
cf2cc11167 | ||
|
|
9e71f529e7 | ||
|
|
4f4f5112b5 | ||
|
|
6dc2965209 | ||
|
|
6d9db4389d | ||
|
|
b5eceadee8 | ||
|
|
b74f2820da | ||
|
|
c69c1aa855 | ||
|
|
d0f2e41874 | ||
|
|
4be56402d8 | ||
|
|
9f0e03c151 | ||
|
|
471a431af5 | ||
|
|
26d8410649 | ||
|
|
8403ad556c | ||
|
|
d26c15f857 | ||
|
|
86ed664e3e | ||
|
|
7df6f40bc4 | ||
|
|
690724bc74 | ||
|
|
08bdbe79b5 | ||
|
|
177f4653c8 | ||
|
|
cc4acb3916 | ||
|
|
a105f395dd | ||
|
|
a06e4f46cf | ||
|
|
07ae3f7711 | ||
|
|
46c4634868 | ||
|
|
0ce35d4c38 | ||
|
|
a5ebc12580 | ||
|
|
270f6e366f | ||
|
|
acf215c1bf | ||
|
|
8a98cd7d19 | ||
|
|
3a5b48b608 | ||
|
|
d735ad7dd7 | ||
|
|
25dd0f1504 | ||
|
|
8e14c6cef5 | ||
|
|
f220904742 | ||
|
|
9f7abe4413 | ||
|
|
de730205af | ||
|
|
dc74c07ef4 | ||
|
|
36b11d6b00 | ||
|
|
9a77e9c52a | ||
|
|
a873f0e0dc | ||
|
|
42dc2dc027 | ||
|
|
14803b2625 | ||
|
|
79f70d8b4b | ||
|
|
7911cff96a | ||
|
|
1a5180551e | ||
|
|
fe501965bf | ||
|
|
fb82cadc82 | ||
|
|
f5b6effeb3 | ||
|
|
63c2901ae3 | ||
|
|
32e3765b52 | ||
|
|
e697881581 | ||
|
|
4f62095e8e | ||
|
|
5234448ae5 | ||
|
|
0356aef7b2 | ||
|
|
f709dc99f6 | ||
|
|
f284c67988 | ||
|
|
dd4ee1dcb5 | ||
|
|
2d77813dab | ||
|
|
d22a255143 | ||
|
|
6703bbeeec | ||
|
|
d444c591ec | ||
|
|
f7ddb6cae0 | ||
|
|
7ffc02e887 | ||
|
|
cbacb27bb1 | ||
|
|
03a08a320a | ||
|
|
6c71bccc27 | ||
|
|
728a66bac2 | ||
|
|
c850b9ce1e | ||
|
|
9a33663f1e | ||
|
|
791bc20e8e | ||
|
|
b088918cd9 | ||
|
|
45223be64a | ||
|
|
b22dfdfa4c | ||
|
|
ce219f0e40 | ||
|
|
a0f26d38b2 | ||
|
|
4efdcfe0fc | ||
|
|
806ccf950f | ||
|
|
3f2938df1c | ||
|
|
b96b78fa0b | ||
|
|
f816bf005e | ||
|
|
791ad6ce37 | ||
|
|
4bc802cee2 | ||
|
|
4662c8c896 | ||
|
|
7058560a03 | ||
|
|
79a5999ad5 | ||
|
|
1aeded5da2 | ||
|
|
e8d1d882c2 | ||
|
|
39dca810a5 | ||
|
|
97f94688f1 | ||
|
|
0b502cbc99 | ||
|
|
a8eb8f9ab1 | ||
|
|
6471f4365a | ||
|
|
48999f5699 | ||
|
|
5ad64a596b | ||
|
|
df60bd0b40 | ||
|
|
41e6f1cf48 | ||
|
|
0802ba191a | ||
|
|
ad57ed241d | ||
|
|
fed1383ced | ||
|
|
c39d1f433a | ||
|
|
f17cb420e3 | ||
|
|
c2396f093b | ||
|
|
bdf65dcfd1 | ||
|
|
2aefbe17ee | ||
|
|
91ff80a255 | ||
|
|
5e43d69d8e | ||
|
|
7f10e20a41 | ||
|
|
4482b92ccd | ||
|
|
d3c39caefb | ||
|
|
24c5dce24d | ||
|
|
9971e512c8 | ||
|
|
0c77ed9bba | ||
|
|
e759c96ade | ||
|
|
f602b75268 | ||
|
|
d4d9eaf773 | ||
|
|
68a800cdfc | ||
|
|
ff45d1a875 | ||
|
|
11d0378060 | ||
|
|
49000c6140 | ||
|
|
24bba484ca | ||
|
|
c8d0d85937 | ||
|
|
c692f7c647 | ||
|
|
080463458b | ||
|
|
08aaf4dd62 | ||
|
|
8ed4f959cb | ||
|
|
e5f7778861 | ||
|
|
ad1e517472 | ||
|
|
17ed5eb46b | ||
|
|
1a2faf02da | ||
|
|
2d086a05b2 | ||
|
|
6ef4603a7c | ||
|
|
d6522338aa | ||
|
|
df154a45b6 | ||
|
|
c5dbea334d | ||
|
|
85f535eb5d | ||
|
|
9334514c71 | ||
|
|
d438755616 | ||
|
|
54595fb7b6 | ||
|
|
4ee8d5a099 | ||
|
|
abe5fae4c4 | ||
|
|
aeb48661b1 | ||
|
|
4bdf68c2b6 | ||
|
|
c5a0e378fd | ||
|
|
538f656640 | ||
|
|
a323c42acf | ||
|
|
095101ad2f | ||
|
|
7ee5ebd506 | ||
|
|
f90ef6297d | ||
|
|
4167107a91 | ||
|
|
90c1cb1f52 | ||
|
|
8c614b84a7 | ||
|
|
7d1a501ad8 | ||
|
|
48589274c8 | ||
|
|
83f7676db4 | ||
|
|
c8b30810f6 | ||
|
|
f71fa7d97a | ||
|
|
4c7c4c00ab | ||
|
|
d2645a9c08 | ||
|
|
ae341d66ba | ||
|
|
fce4def951 | ||
|
|
ba0964f772 | ||
|
|
e346417c29 | ||
|
|
5fcc184a88 | ||
|
|
f6d3f89b5b | ||
|
|
106e09a7a2 | ||
|
|
d1b4600392 | ||
|
|
4204dfa624 | ||
|
|
d7fd5be068 | ||
|
|
735bb397ec | ||
|
|
8b5a2b713c | ||
|
|
f2ec9327fd | ||
|
|
5331e10c96 | ||
|
|
7090f02a52 | ||
|
|
12152a6810 | ||
|
|
c0c2d3d1c9 | ||
|
|
858844e81a | ||
|
|
1ff2731121 | ||
|
|
70db7662bb | ||
|
|
bb57ff72e7 | ||
|
|
8df79fd624 | ||
|
|
7946c08c72 | ||
|
|
6f20ef7273 | ||
|
|
657ae77dde | ||
|
|
4ff80dd257 | ||
|
|
9bb1069d26 | ||
|
|
730be25948 | ||
|
|
78d8fea7f5 | ||
|
|
22ed78c87b | ||
|
|
f5331d4c50 | ||
|
|
f32fe0ec5b | ||
|
|
3b914fbc2e | ||
|
|
533df0eb16 | ||
|
|
9c38165afb | ||
|
|
7b506320b4 | ||
|
|
2b7c497c3e | ||
|
|
4dee1309eb | ||
|
|
f3f8909e45 | ||
|
|
d45cd79422 | ||
|
|
7cbb58ff32 | ||
|
|
9242e1fb47 | ||
|
|
f5ce74d0ee | ||
|
|
3114280e83 | ||
|
|
c7b8b36f4a | ||
|
|
60ee194cc9 | ||
|
|
4f50593eac | ||
|
|
8818dd21c6 | ||
|
|
db8ee298d9 | ||
|
|
9eef99a435 | ||
|
|
3d027ed374 | ||
|
|
271b5d445d | ||
|
|
2457d14ae9 | ||
|
|
d70eb17a72 | ||
|
|
ddd619babc | ||
|
|
5d27ee830e | ||
|
|
b73b83a49f | ||
|
|
549debf8bf | ||
|
|
212b9a43a9 | ||
|
|
f092a23ee4 | ||
|
|
25c357a423 | ||
|
|
a77246a1db | ||
|
|
f00ce31648 | ||
|
|
dc57a491dd | ||
|
|
c9b7266959 | ||
|
|
0dd2111cb9 | ||
|
|
c39b048398 | ||
|
|
87fbf15e2b | ||
|
|
db1cfaa1a6 | ||
|
|
905b750877 | ||
|
|
be14fe2603 | ||
|
|
9739609de4 | ||
|
|
d7f101ee30 | ||
|
|
20f7d595a1 | ||
|
|
e909dc81ad | ||
|
|
a510779ea1 | ||
|
|
c799c199c5 | ||
|
|
0818e9ade2 | ||
|
|
e60a8bed8b | ||
|
|
17fdcd7d1f | ||
|
|
6412e1a997 | ||
|
|
1674598480 | ||
|
|
23ebf36244 | ||
|
|
342db06685 | ||
|
|
35b9f813c4 | ||
|
|
aafb326662 | ||
|
|
aa1b309b26 | ||
|
|
2c355525df | ||
|
|
bd20a499d3 | ||
|
|
5fec5d5239 | ||
|
|
5b3aaa463d | ||
|
|
a14b22b1ef | ||
|
|
50aa549311 | ||
|
|
bf0aef59cd | ||
|
|
b2ddcd0cf3 | ||
|
|
947dba5934 | ||
|
|
572ef04bb4 | ||
|
|
110d6c689c | ||
|
|
f40d6fd5e8 | ||
|
|
49ace44c9b | ||
|
|
cc3bc548c9 | ||
|
|
4816a512bf | ||
|
|
cc3d7ccd7b | ||
|
|
43bc7db7d5 | ||
|
|
b0672aead5 | ||
|
|
086d5a18e8 | ||
|
|
91576f401b | ||
|
|
febcd18eda | ||
|
|
8cc9f0d291 | ||
|
|
41643563f8 | ||
|
|
b6b58eaeba | ||
|
|
e8a5701e73 | ||
|
|
a8f2c8f41a | ||
|
|
241222dcd1 | ||
|
|
380575dae0 | ||
|
|
4d685d5a67 | ||
|
|
96196229f4 | ||
|
|
5e54039028 | ||
|
|
94b2e84c10 | ||
|
|
80d2230c9a | ||
|
|
4fad163b14 | ||
|
|
4d20a68f93 | ||
|
|
90a1d6fdff | ||
|
|
00e1cb707e | ||
|
|
6eeb774a21 | ||
|
|
6c4b8a52dc | ||
|
|
a7e0d8f826 | ||
|
|
d1ccddc1d3 | ||
|
|
d5f0c3bc95 | ||
|
|
da2191f365 | ||
|
|
056643bd96 | ||
|
|
01d5ed297e | ||
|
|
55ab2b8562 | ||
|
|
dae5d0a9f5 | ||
|
|
41ad33310a | ||
|
|
526392c370 | ||
|
|
b38ee9c7c7 | ||
|
|
9f7f1db9b8 | ||
|
|
b7134cb3e4 | ||
|
|
ec78658e46 | ||
|
|
44361af392 | ||
|
|
76130caa6d | ||
|
|
352a78a0c5 | ||
|
|
6020c80a7a | ||
|
|
a2a165a42b | ||
|
|
0f27c1e5ce | ||
|
|
e944ad4369 | ||
|
|
65bee9586d | ||
|
|
31cda8579a | ||
|
|
1dc40b9efc | ||
|
|
6cb3d8ad65 | ||
|
|
225e2732d1 | ||
|
|
768ad001d2 | ||
|
|
866d11988d | ||
|
|
d82ae99b6a | ||
|
|
eb4f6f1b09 | ||
|
|
23341b93c7 | ||
|
|
32b778f961 | ||
|
|
b99f4904b0 | ||
|
|
17b1c12286 | ||
|
|
c79824e358 | ||
|
|
f742feca4b | ||
|
|
de8971bea6 | ||
|
|
569e003827 | ||
|
|
c47c130fc3 | ||
|
|
2db5dd59af | ||
|
|
8a8504dca9 | ||
|
|
7e82c6a802 | ||
|
|
3946fda013 | ||
|
|
db30b45267 | ||
|
|
01377b6013 | ||
|
|
b38be38169 | ||
|
|
b9ac4a1c18 | ||
|
|
bdc6251474 | ||
|
|
479cf5c4a0 | ||
|
|
dbea497cc5 | ||
|
|
94af73a810 | ||
|
|
a7fd6a8e16 | ||
|
|
2b3502161c | ||
|
|
8039ab8281 | ||
|
|
0544a72011 | ||
|
|
34ba09ce1b | ||
|
|
2bfe653ac0 | ||
|
|
aa5a71ae68 | ||
|
|
9067e645b3 | ||
|
|
10d3fc096a | ||
|
|
7fd95ba6f9 | ||
|
|
b4982e07b8 | ||
|
|
6cc2a02df4 | ||
|
|
e707637c88 | ||
|
|
5da89e8ee7 | ||
|
|
f9ff279895 | ||
|
|
4cde9b67c1 | ||
|
|
0f107ff29d | ||
|
|
5e69a9b6f0 | ||
|
|
ec9311c76b | ||
|
|
4c85a7d249 | ||
|
|
09d4c2d26e | ||
|
|
b8dac5dc72 | ||
|
|
73cafc9786 | ||
|
|
c276ae0c5e | ||
|
|
b4ed061a53 | ||
|
|
ddaaa8fc92 | ||
|
|
3a253df58a | ||
|
|
21debe2f77 | ||
|
|
0e4f662dfd | ||
|
|
4a797808c1 | ||
|
|
3a81b822f2 | ||
|
|
f5a7efb324 | ||
|
|
abc3ca184d | ||
|
|
267c022d58 | ||
|
|
0fc2844928 | ||
|
|
7f1fe42ace | ||
|
|
c83a13d680 | ||
|
|
3d7dcb6d0e | ||
|
|
a46f24d6d7 | ||
|
|
2e4f2428f0 | ||
|
|
706cebfeae | ||
|
|
ee7f5e3a07 | ||
|
|
3d0160f21f | ||
|
|
0aa0126caf | ||
|
|
5ecdd37187 | ||
|
|
20cc2ca248 | ||
|
|
5cfd7d65ec | ||
|
|
3b8307b674 | ||
|
|
2f800c960d | ||
|
|
2728b4058b | ||
|
|
b842905572 | ||
|
|
85fa9a8658 | ||
|
|
5ae56481c2 | ||
|
|
8079d7d6af | ||
|
|
7c8eef1715 | ||
|
|
f10b3179c2 | ||
|
|
2e0bb5aa84 | ||
|
|
7ae8d42731 | ||
|
|
a9e65af77b | ||
|
|
d21ecc975f | ||
|
|
452509281a | ||
|
|
2e3602eee6 | ||
|
|
7dfd535841 | ||
|
|
39d928b5b1 | ||
|
|
525695072a | ||
|
|
972a4ce789 | ||
|
|
879aadc068 |
@@ -1,6 +1,7 @@
|
||||
# turbo
|
||||
.turbo/
|
||||
.vscode/**/*
|
||||
**/.vscode/
|
||||
*.code-workspace
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
@@ -27,6 +28,7 @@ settings.json
|
||||
.npmrc
|
||||
.cursor
|
||||
.agent
|
||||
CLAUDE.MD
|
||||
|
||||
apps/**/tsconfig.tsbuildinfo
|
||||
packages/**/tsconfig.tsbuildinfo
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
// IntelliSense를 사용하여 가능한 특성에 대해 알아보세요.
|
||||
// 기존 특성에 대한 설명을 보려면 가리킵니다.
|
||||
// 자세한 내용을 보려면 https://go.microsoft.com/fwlink/?linkid=830387을(를) 방문하세요.
|
||||
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
@@ -14,13 +12,91 @@
|
||||
"dev:as"
|
||||
],
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/**",
|
||||
"!**/node_modules/**"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"sourceMaps": true,
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--inspect"
|
||||
"NODE_OPTIONS": "--inspect --max-old-space-size=8192"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "dev:as 서버 컴포넌트 디버그 (node-terminal)",
|
||||
"command": "pnpm --filter as dev",
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "dev:as 통합 디버그 (Server + Client)",
|
||||
"command": "pnpm --filter as dev",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"serverReadyAction": {
|
||||
"action": "debugWithChrome",
|
||||
"pattern": "- Local:\\s+(https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"webRoot": "${workspaceFolder}/apps/as"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Playwright: 현재 활성화된 테스트 디버그",
|
||||
"runtimeExecutable": "pnpm",
|
||||
"runtimeArgs": [
|
||||
"--filter",
|
||||
"cs",
|
||||
"exec",
|
||||
"playwright",
|
||||
"test",
|
||||
"${relativeFile}"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Playwright: cs 로그인 테스트 디버그",
|
||||
"runtimeExecutable": "pnpm",
|
||||
"runtimeArgs": [
|
||||
"--filter",
|
||||
"cs",
|
||||
"exec",
|
||||
"playwright",
|
||||
"test",
|
||||
"tests/login.spec.ts"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "dev:cs 통합 디버그 (Server + Client)",
|
||||
"command": "pnpm --filter cs dev",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"serverReadyAction": {
|
||||
"action": "debugWithChrome",
|
||||
"pattern": "- Local:\\s+(https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"webRoot": "${workspaceFolder}/apps/cs"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -33,13 +109,18 @@
|
||||
"dev:cs"
|
||||
],
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/**",
|
||||
"!**/node_modules/**"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"sourceMaps": true,
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--inspect"
|
||||
"NODE_OPTIONS": "--inspect --max-old-space-size=8192"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -49,7 +130,8 @@
|
||||
"port": 9229,
|
||||
"restart": true,
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
@@ -63,7 +145,15 @@
|
||||
"name": "dev:cs 클라이언트 디버그 (Chrome)",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}/apps/cs",
|
||||
"sourceMaps": true
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/apps/cs/**",
|
||||
"!**/node_modules/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
@@ -75,13 +165,31 @@
|
||||
"dev:st"
|
||||
],
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/**",
|
||||
"!**/node_modules/**"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"sourceMaps": true,
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--inspect"
|
||||
"NODE_OPTIONS": "--inspect --max-old-space-size=8192"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "dev:st 통합 디버그 (Server + Client)",
|
||||
"command": "pnpm --filter st dev",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"serverReadyAction": {
|
||||
"action": "debugWithChrome",
|
||||
"pattern": "- Local:\\s+(https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"webRoot": "${workspaceFolder}/apps/st"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"chat.tools.terminal.autoApprove": {
|
||||
"pnpm": true
|
||||
}
|
||||
},
|
||||
"playwright.configs": [
|
||||
"apps/cs/playwright.config.ts"
|
||||
]
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { Providers } from "@/app/providers";
|
||||
import "@/app/styles/globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "행정지원시스템",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
description: "행정지원시스템",
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@/app/styles/globals.css";
|
||||
|
||||
export const metadata = {
|
||||
title: "디지털 행정지원 시스템",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
};
|
||||
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { DesignListPage } from "@/pages/design-cnstn";
|
||||
|
||||
export default DesignListPage;
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense>
|
||||
<DesignListPage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { FnrsListPage } from "@/pages/fnrs-cnsltn";
|
||||
|
||||
export default FnrsListPage;
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense>
|
||||
<FnrsListPage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { InstlListPage } from "@/pages/instl-sncn";
|
||||
|
||||
export default InstlListPage;
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense>
|
||||
<InstlListPage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { NtitlkSndngPage } from "@/pages/ntitlk-sndng";
|
||||
|
||||
export default NtitlkSndngPage;
|
||||
@@ -1,3 +1,11 @@
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { SewerBscPlanListPage } from "@/pages/sewer-bsc-plan";
|
||||
|
||||
export default SewerBscPlanListPage;
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense>
|
||||
<SewerBscPlanListPage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Providers } from "@/app/providers";
|
||||
import "@/app/styles/globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "디지털 행정지원 시스템",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
description: "디지털 행정지원 시스템",
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import Image from "next/image";
|
||||
import "@/app/styles/globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "디지털 행정지원 시스템",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
description: "페이지가 존재하지 않습니다. 관리자에게 문의하여 주십시오.",
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
# PM2 배포 및 프로세스 관리 가이드
|
||||
|
||||
본 문서는 ONA 프론트엔드 모노레포 프로젝트(`frontend_as`)에서 **PM2를 활용한 애플리케이션 배포, 실행, 중지 및 모니터링 명령**을 체계적으로 정리한 가이드입니다.
|
||||
|
||||
---
|
||||
pnpm build:common && pnpm build:as && pm2 reload as2-app
|
||||
pnpm --filter=as... build
|
||||
pnpm --filter=as... build && pm2 reload as2-app
|
||||
## 1. 개요 및 아키텍처
|
||||
|
||||
- **설정 파일**: 프로젝트 루트의 `ecosystem.config.js`
|
||||
- **배포 워크플로우**: `.gitea/workflows/deploy.yml` (Gitea Actions 자동 배포)
|
||||
- **실행 모드**: `cluster` (2개 인스턴스로 로드밸런싱 및 무중단 롤링 배포 지원)
|
||||
- **운영 대상**:
|
||||
- `as2-app` (또는 `as-app`, `cs-app`, `st-app`, `cm-app` 등)
|
||||
|
||||
---
|
||||
|
||||
## 2. CI/CD 자동 배포 흐름
|
||||
|
||||
Gitea 저장소의 `deploy` 브랜치로 푸시될 때 원격 서버(192.168.0.4)에서 다음 스크립트가 순차 실행됩니다:
|
||||
|
||||
```bash
|
||||
cd ~/frontend
|
||||
git restore . # 변경된 로컬 파일 원복
|
||||
git clean -fd # 추적되지 않는 불필요한 파일/디렉터리 정리
|
||||
git pull origin deploy # 최신 deploy 브랜치 코드 반영
|
||||
pnpm install # 의존성 패키지 설치
|
||||
pnpm turbo build # 프로덕션 빌드 (.next 산출물 생성)
|
||||
pm2 reload all # 무중단 서비스 리로드 (클러스터 롤링 재시작)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 수동 배포 (Manual Deploy) 명령어
|
||||
|
||||
서버 터미널에 직접 접속하여 수동으로 최신 코드를 배포할 때 사용하는 명령어입니다.
|
||||
|
||||
### 3.1 전체 애플리케이션 수동 배포
|
||||
```bash
|
||||
# 1. 프로젝트 루트 이동
|
||||
cd ~/frontend
|
||||
|
||||
# 2. 최신 코드 가져오기
|
||||
git pull origin deploy
|
||||
|
||||
# 3. 의존성 설치 및 전체 빌드
|
||||
pnpm install
|
||||
pnpm turbo build
|
||||
|
||||
# 4. PM2 전체 무중단 리로드
|
||||
pm2 reload all
|
||||
```
|
||||
|
||||
### 3.2 `as` (행정지원) 앱만 단독 빌드 및 배포
|
||||
```bash
|
||||
cd ~/frontend
|
||||
|
||||
# as 앱만 타겟 빌드
|
||||
pnpm --filter as build
|
||||
|
||||
# as2-app 프로세스만 무중단 리로드
|
||||
pm2 reload as2-app
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. PM2 실행 및 프로세스 등록 (Start & Save)
|
||||
|
||||
최초 서버 환경 구성 시 또는 프로세스 목록에서 삭제된 후 새롭게 등록할 때 실행합니다.
|
||||
|
||||
```bash
|
||||
# 1. 로그 디렉터리 사전 생성 (필수)
|
||||
mkdir -p apps/as/logs
|
||||
|
||||
# 2. ecosystem.config.js 기반 전체 앱 최초 실행
|
||||
pm2 start ecosystem.config.js
|
||||
|
||||
# 3. 특정 앱(as2-app)만 지정하여 최초 실행
|
||||
pm2 start ecosystem.config.js --only as2-app
|
||||
|
||||
# 4. 서버 재부팅 시에도 PM2 프로세스가 자동 복구되도록 현재 상태 저장 (필수!)
|
||||
pm2 save
|
||||
|
||||
# 5. OS 부팅 시 PM2 데몬 자동 등록 (최초 1회 설정)
|
||||
pm2 startup
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 중지, 재시작 및 삭제 명령어
|
||||
|
||||
### 5.1 재시작 (Restart vs Reload)
|
||||
> **TIP: 운영 환경에서는 항상 `reload`를 권장합니다.**
|
||||
> - `reload`: 0-second downtime(무중단). 클러스터의 인스턴스를 하나씩 순차적으로 재시작하여 사용자 요청이 끊기지 않음.
|
||||
> - `restart`: 모든 인스턴스를 일시에 강제 종료 후 다시 띄우므로 짧은 다운타임 발생.
|
||||
|
||||
```bash
|
||||
# [권장] 무중단 재로드 (운영 배포 시)
|
||||
pm2 reload as2-app
|
||||
pm2 reload all
|
||||
|
||||
# 강제 재시작 (설정 변경이 반영되지 않거나 프로세스가 비정상일 때)
|
||||
pm2 restart as2-app
|
||||
pm2 restart all
|
||||
```
|
||||
|
||||
### 5.2 프로세스 중지 (Stop)
|
||||
프로세스를 메모리에서 내리고 실행을 일시 중단합니다 (PM2 목록에는 유지됨).
|
||||
|
||||
```bash
|
||||
# 특정 앱 중지
|
||||
pm2 stop as2-app
|
||||
|
||||
# 전체 앱 중지
|
||||
pm2 stop all
|
||||
```
|
||||
|
||||
### 5.3 프로세스 목록에서 제거 (Delete)
|
||||
PM2 관리 대상 목록에서 프로세스를 완전히 삭제합니다.
|
||||
|
||||
```bash
|
||||
# 특정 앱 삭제
|
||||
pm2 delete as2-app
|
||||
|
||||
# 전체 앱 삭제
|
||||
pm2 delete all
|
||||
|
||||
# 삭제 상태를 영구 저장할 경우
|
||||
pm2 save
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 상태 모니터링 및 로그 확인
|
||||
|
||||
### 6.1 프로세스 상태 확인
|
||||
```bash
|
||||
# 간략한 프로세스 상태 표 확인 (online, stopped, cpu, memory 등)
|
||||
pm2 status
|
||||
# 또는
|
||||
pm2 list
|
||||
|
||||
# 특정 앱 상세 정보 확인 (환경변수, 파일 경로, 재시작 횟수 등)
|
||||
pm2 show as2-app
|
||||
```
|
||||
|
||||
### 6.2 실시간 로그 스트리밍 (Logs)
|
||||
```bash
|
||||
# 특정 앱 실시간 로그 확인 (표준 출력 + 에러 출력)
|
||||
pm2 logs as2-app
|
||||
|
||||
# 최근 100줄 출력 후 실시간 스트리밍
|
||||
pm2 logs as2-app --lines 100
|
||||
|
||||
# 에러 로그만 확인
|
||||
pm2 logs as2-app --err
|
||||
|
||||
# 전체 프로세스 로그 확인
|
||||
pm2 logs
|
||||
|
||||
# 이전 로그 비우기(Flush)
|
||||
pm2 flush
|
||||
```
|
||||
|
||||
### 6.3 터미널 대시보드 모니터링 (Monit)
|
||||
CPU 및 메모리 실시간 사용량을 터미널 대시보드로 확인합니다.
|
||||
```bash
|
||||
pm2 monit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 주요 트러블슈팅 (FAQ)
|
||||
|
||||
### Q1. `EADDRINUSE: address already in use :::3004` 포트 충돌 발생
|
||||
- **원인**: 로컬 개발 서버(`pnpm --filter as dev`) 또는 이전 Node 프로세스가 3004번 포트를 계속 점유 중인 상태입니다.
|
||||
- **해결**:
|
||||
```bash
|
||||
# 3004 포트 사용 중인 PID 확인
|
||||
lsof -i :3004
|
||||
|
||||
# 해당 프로세스 종료
|
||||
kill -9 <PID>
|
||||
|
||||
# PM2 리로드
|
||||
pm2 reload as2-app
|
||||
```
|
||||
|
||||
### Q2. `Could not find a production build in the '.next' directory`
|
||||
- **원인**: Next.js 프로덕션 실행 전에 빌드 산출물(`.next`)이 생성되지 않았습니다.
|
||||
- **해결**:
|
||||
```bash
|
||||
pnpm --filter as build
|
||||
pm2 reload as2-app
|
||||
```
|
||||
|
||||
### Q3. `logs` 디렉터리 관련 에러 발생
|
||||
- **원인**: `ecosystem.config.js`의 `cwd: './apps/as'` 설정에 따라 `./logs` 경로가 없을 경우 발생할 수 있습니다.
|
||||
- **해결**:
|
||||
```bash
|
||||
mkdir -p apps/as/logs
|
||||
```
|
||||
@@ -0,0 +1,181 @@
|
||||
# Next.js 프로덕션 빌드 에러 원인 분석 및 조치 보고서
|
||||
|
||||
본 문서는 `apps/as` 프로덕션 빌드(`pnpm build:as` / `next build`) 과정에서 발생한 ESLint Hook 규칙 위반 및 TypeScript 컴파일 에러의 원인과 이를 해결한 조치 내역을 정리한 문서입니다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요 및 장애 상황
|
||||
|
||||
- **대상 모듈**: `apps/as` (행정지원 프론트엔드 애플리케이션)
|
||||
- **실행 명령**: `pnpm build:as` (또는 `pnpm --filter as build`)
|
||||
- **발생 시점**: 2026-10-20
|
||||
- **장애 영향**: Next.js 프로덕션 최적화 빌드 실패로 인해 배포 산출물(`.next`) 생성이 중단됨
|
||||
|
||||
### 빌드 실패 로그 요약
|
||||
```text
|
||||
as:build: Failed to compile.
|
||||
|
||||
as:build: ./src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx
|
||||
as:build: 159:35 Error: React Hook "useStore" is called conditionally. React Hooks must be called in the exact same order in every component render. react-hooks/rules-of-hooks
|
||||
|
||||
./src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx:164:5
|
||||
Type error: Type 'boolean' has no properties in common with type '{ enabled?: boolean | undefined; structuralSharing?: boolean | undefined; }'.
|
||||
|
||||
./src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx:215:29
|
||||
Type error: Property 'atrzSeq' does not exist on type 'ElaprDocPrgrsClientResponseDto'. Did you mean 'atrzSeqNo'?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 근본 원인 분석 (Root Cause Analysis)
|
||||
|
||||
### 2.1 React Hook 규칙 위반 (`react-hooks/rules-of-hooks`)
|
||||
- **위치**: `apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx` (158~159라인)
|
||||
- **원인 코드**:
|
||||
```tsx
|
||||
const storeContext = useContext(StoreContext);
|
||||
const userInfo = storeContext ? useStore(storeContext.userStore, (state) => state.userInfo) : null;
|
||||
```
|
||||
- **문제점**:
|
||||
- React Hooks는 조건문, 반복문, 삼항 연산자 내부에서 호출될 수 없으며, 모든 렌더링마다 항상 동일한 순서로 호출되어야 합니다.
|
||||
- 삼항 연산자 내에서 `useStore`를 호출하면서 `react-hooks/rules-of-hooks` 에러가 발생하여 빌드가 즉시 차단되었습니다.
|
||||
- 또한 프로젝트 전역에서 제공하는 표준 훅인 `useUserStore`(`@/shared/lib`)가 이미 존재함에도 불구하고 이를 사용하지 않고 수동으로 컨텍스트를 조회하여 분기 처리하고 있었습니다.
|
||||
|
||||
### 2.2 `useElaprBranchesByDocNo` 훅 옵션 매개변수 타입 불일치
|
||||
- **원인 코드**:
|
||||
```tsx
|
||||
const { data: elaprBranchList = [] } = useElaprBranchesByDocNo(
|
||||
fnrsCnsltnDocNo,
|
||||
open && Boolean(isForkJoin), // boolean 직접 전달
|
||||
);
|
||||
```
|
||||
- **문제점**:
|
||||
- `useElaprBranchesByDocNo`의 시그니처는 `(elaprDocNo, options?: { enabled?: boolean; structuralSharing?: boolean })`로 선언되어 있어, `boolean` 대신 객체 형태(`{ enabled: ... }`)로 전달해야 합니다.
|
||||
|
||||
### 2.3 전자결재 결재선 DTO 필드명 불일치
|
||||
- **원인 코드**:
|
||||
```tsx
|
||||
if (currentUserPrgrs?.atrzSeq && Number(currentUserPrgrs.atrzSeq) > 1) return true;
|
||||
```
|
||||
- **문제점**:
|
||||
- `ElaprDocPrgrsClientResponseDto` 인터페이스(`src/shared/model/pbadms-sprt.ts`)에 선언된 정식 결재 순번 필드명은 `atrzSeqNo?: number`입니다.
|
||||
- 기존 코드가 `atrzSeq`로 접근하여 TypeScript 컴파일 에러가 발생했습니다.
|
||||
|
||||
### 2.4 `PwrkCnsltnDocEvlDialog`의 필수 prop `onSaved` 누락
|
||||
- **원인 코드**:
|
||||
```tsx
|
||||
<PwrkCnsltnDocEvlDialog
|
||||
open={isEvlDialogOpen}
|
||||
pwrkCnsltnDocNo={completedDraft.pwrkCnsltnDocNo}
|
||||
onClose={() => setIsEvlDialogOpen(false)}
|
||||
/>
|
||||
```
|
||||
- **문제점**:
|
||||
- `PwrkCnsltnDocEvlDialogProps`에 `onSaved: () => void;`가 필수로 선언되어 있었으나, 호출부에서 `onSaved` 콜백을 넘기지 않아 `Property 'onSaved' is missing` 컴파일 에러가 발생했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 조치 내역
|
||||
|
||||
### 3.1 컴포넌트 코드 수정 (`pwrk-cnsltn-doc-mng-dialog.tsx`)
|
||||
1. **표준 `useUserStore` 훅 적용**:
|
||||
- `useStore`, `StoreContext`, `useContext`를 제거하고, `@/shared/lib`의 `useUserStore`를 무조건부로 호출하도록 변경.
|
||||
2. **`useElaprBranchesByDocNo` 옵션 객체화**:
|
||||
- `{ enabled: open && Boolean(isForkJoin) }` 형태로 수정.
|
||||
3. **결재 순번 속성 방어적 매핑**:
|
||||
- `atrzSeqNo`를 우선 참조하고 레거시/모의 데이터의 `atrzSeq`도 호환되도록 fallback 처리.
|
||||
4. **`PwrkCnsltnDocEvlDialog`의 `onSaved` prop 연결 및 선택적(Optional) 처리**:
|
||||
- `pwrk-cnsltn-doc-evl-dialog.tsx`: `onSaved?: () => void;`로 변경하고 `onSaved?.()` 호출로 방어.
|
||||
- `pwrk-cnsltn-doc-mng-dialog.tsx`: `<PwrkCnsltnDocEvlDialog ... onSaved={() => setIsEvlDialogOpen(false)} />` 콜백 전달.
|
||||
|
||||
#### 핵심 변경 사항 (Diff)
|
||||
```diff
|
||||
--- a/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx
|
||||
+++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx
|
||||
@@ -1,14 +1,11 @@
|
||||
"use client";
|
||||
|
||||
-import { useContext, useEffect, useMemo, useState } from "react";
|
||||
+import { useEffect, useMemo, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { PanelRightClose, PanelRightOpen } from "lucide-react";
|
||||
-import { useStore } from "zustand";
|
||||
-
|
||||
-import { getDefaultFileKey, useAlertStore, useTus } from "@/shared/lib";
|
||||
-import { StoreContext } from "@/shared/providers";
|
||||
+import { getDefaultFileKey, useAlertStore, useTus, useUserStore } from "@/shared/lib";
|
||||
|
||||
@@ -155,8 +152,7 @@ function PwrkCnsltnDocMngDialog({
|
||||
const queryClient = useQueryClient();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
- const storeContext = useContext(StoreContext);
|
||||
- const userInfo = storeContext ? useStore(storeContext.userStore, (state) => state.userInfo) : null;
|
||||
+ const userInfo = useUserStore((state) => state.userInfo);
|
||||
|
||||
@@ -165,7 +161,7 @@ function PwrkCnsltnDocMngDialog({
|
||||
const { data: elaprBranchList = [] } = useElaprBranchesByDocNo(
|
||||
fnrsCnsltnDocNo,
|
||||
- open && Boolean(isForkJoin),
|
||||
+ { enabled: open && Boolean(isForkJoin) },
|
||||
);
|
||||
|
||||
@@ -216,11 +212,19 @@ function PwrkCnsltnDocMngDialog({
|
||||
if (effectiveBranchSeCd === "TOTAL_AGG") {
|
||||
if (effectiveAtrzSeList.includes("ATRZ04")) return false;
|
||||
if (effectiveAtrzSeList.some((cd) => APPROVER_ATRZ_SE_COM_CDS.has(cd))) return true;
|
||||
- if (currentUserPrgrs?.atrzSeq && Number(currentUserPrgrs.atrzSeq) > 1) return true;
|
||||
+ const userSeq = currentUserPrgrs?.atrzSeqNo ?? (currentUserPrgrs as Record<string, unknown> | undefined)?.atrzSeq;
|
||||
+ if (userSeq && Number(userSeq) > 1) return true;
|
||||
if (effectiveAtrzSeList.length > 0 && !effectiveAtrzSeList.includes("ATRZ04")) return true;
|
||||
}
|
||||
return false;
|
||||
```
|
||||
|
||||
### 3.2 단위 테스트 모킹 보완 (`pwrk-cnsltn-doc-mng-dialog.test.tsx`)
|
||||
- 단위 테스트 환경에서 `StoreProvider` 미감싸임 에러(`useUserStore must be used within StoreProvider`)가 발생하지 않도록 `@/shared/lib` 모킹 객체에 `useUserStore`를 추가.
|
||||
```tsx
|
||||
let mockUserInfo: any = null;
|
||||
|
||||
vi.mock("@/shared/lib", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/shared/lib")>();
|
||||
return {
|
||||
...actual,
|
||||
getDefaultFileKey: () => "mock-key",
|
||||
useAlertStore: () => vi.fn(),
|
||||
useUserStore: (selector: (state: { userInfo: any }) => any) =>
|
||||
selector ? selector({ userInfo: mockUserInfo }) : { userInfo: mockUserInfo },
|
||||
useTus: () => ({
|
||||
uploadFileAsync: vi.fn(),
|
||||
isUploading: {},
|
||||
uploads: {},
|
||||
}),
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 검증 결과
|
||||
|
||||
| 검증 단계 | 명령어 | 결과 | 비고 |
|
||||
|---|---|---|---|
|
||||
| **1. 정적 타입 검사** | `pnpm --filter as exec tsc --noEmit` | **성공 (Exit 0)** | 컴파일 에러 0건 |
|
||||
| **2. ESLint 린트 검사** | `pnpm --filter as lint` | **성공 (Exit 0)** | Hook 위반 에러 해결 |
|
||||
| **3. 단위 테스트 (Vitest)** | `pnpm --filter as exec vitest run` | **전체 통과 (73/73 Tests)** | 14개 테스트 파일 100% Pass |
|
||||
| **4. 프로덕션 빌드** | `pnpm build:as` | **성공 (Exit 0)** | 22/22 정적 페이지 최적화 완료 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 재발 방지 표준 가이드
|
||||
|
||||
1. **React Hook 호출 규칙 준수**:
|
||||
- `useUserStore`, `useQuery`, `useStore` 등 모든 React Hook은 반드시 컴포넌트 함수의 최상단(Top-level)에서 무조건부로 호출해야 합니다.
|
||||
- 사용자 정보가 필요할 경우 개별 컨텍스트 접근 대신 표준 훅인 `useUserStore((state) => state.userInfo)`를 사용합니다.
|
||||
|
||||
2. **React Query 커스텀 훅 매개변수 규칙**:
|
||||
- 조건부 쿼리 실행이 필요한 경우 boolean 값을 단독으로 넘기지 않고 `{ enabled: boolean }` 형태의 옵션 객체 인터페이스를 유지합니다.
|
||||
|
||||
3. **엔티티 DTO 필드명 엄격 확인**:
|
||||
- 전자결재 결재선 DTO는 백엔드 명세에 따라 `atrzSeqNo`를 표준으로 사용하므로 레거시 명칭(`atrzSeq`)을 사용하지 않도록 타입을 확인합니다.
|
||||
@@ -0,0 +1,160 @@
|
||||
# 개발(Dev) 및 운영(Production) 실행 가이드 & 환경변수 관리
|
||||
|
||||
본 문서는 `apps/as` 애플리케이션의 **개발(Dev) 모드** 및 **운영(Production) 모드** 실행 방법과 **환경변수(.env) 변경 시의 재빌드 규칙**을 상세히 정리한 가이드입니다.
|
||||
|
||||
---
|
||||
|
||||
## 1. Dev 모드 vs Production 모드 비교
|
||||
|
||||
| 구분 | 개발 모드 (Dev) | 운영 모드 (Production) |
|
||||
|---|---|---|
|
||||
| **실행 목적** | 로컬 개발, 디버깅, 실시간 코드 수정 | 실서버 운영, 성능 최적화, 안정성 보장 |
|
||||
| **사전 빌드 필요 여부** | **불필요** (즉시 실행) | **필수** (`.next` 번들 생성 선행) |
|
||||
| **코드 반영 방식** | 핫 리로딩(HMR, 저장 즉시 반영) | 빌드 시점의 번들링 코드 고정 서빙 |
|
||||
| **참조 환경변수** | `.env.local` > `.env.development` | `.env.local` > `.env.production` |
|
||||
| **프로세스 관리** | Node 단일 프로세스 (터미널) | **PM2 Cluster 모드** (2개 인스턴스 무중단) |
|
||||
| **서비스 포트** | 기본 `3004` 포트 | 기본 `3004` 포트 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 환경변수(.env) 변경 시 재빌드 필수 여부
|
||||
|
||||
### 2.1 결론: `.env.production` 변경 시 반드시 재빌드 필요
|
||||
Next.js 환경에서는 `.env.production`을 수정했을 때 단순히 PM2 재시작(`pm2 reload` / `pm2 restart`)만으로는 변경 사항이 브라우저에 반영되지 않으며, **반드시 빌드(`next build`)를 다시 수행해야 합니다.**
|
||||
|
||||
### 2.2 이유: Next.js의 환경변수 인라이닝(Inlining) 원리
|
||||
1. **`NEXT_PUBLIC_*` 변수의 빌드타임 주입**:
|
||||
- `NEXT_PUBLIC_SERVER_API_URL`, `NEXT_PUBLIC_PROXY_HOST`, `NEXT_PUBLIC_WEBHYP_CTRL_URL` 등 브라우저에서 참조하는 변수들은 `next build` 시점에 생성되는 클라이언트 JavaScript 파일(`.next/static/...`) 안에 **문자열 값으로 직접 하드코딩(Inlining)**됩니다.
|
||||
- 따라서 코드를 다시 빌드하지 않으면 예전 주소나 설정값이 그대로 유지됩니다.
|
||||
2. **정적 페이지(SSG) 생성 시점의 값 고정**:
|
||||
- 빌드 시 정적 생성된 페이지(`Generating static pages (22/22)`)는 빌드 당시 읽어들인 환경변수 값을 기준으로 생성되므로, 런타임에 환경변수를 바꿔도 페이지가 갱신되지 않습니다.
|
||||
|
||||
### 2.3 환경변수 우선순위 주의사항
|
||||
Next.js의 환경변수 로딩 우선순위는 다음과 같습니다:
|
||||
|
||||
```text
|
||||
[운영 환경: NODE_ENV=production]
|
||||
1순위: .env.production.local
|
||||
2순위: .env.local (NODE_ENV=test 제외 항상 최우선)
|
||||
3순위: .env.production
|
||||
4순위: .env
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> 실서버 환경에 `.env.local` 파일이 존재할 경우 `.env.production`보다 우선 적용됩니다. `.env.production`을 수정했는데도 반영되지 않는다면 같은 경로에 `.env.local`이 존재하는지 확인하십시오.
|
||||
|
||||
---
|
||||
|
||||
## 3. Dev (개발 모드) 실행 방법
|
||||
|
||||
사전 빌드 과정 없이 소스 코드를 실시간으로 수정하며 개발할 때 사용합니다.
|
||||
|
||||
### 3.1 프로젝트 루트 디렉터리에서 실행
|
||||
```bash
|
||||
# 루트 디렉터리: ~/frontend_as
|
||||
pnpm dev:as
|
||||
|
||||
# 또는 pnpm filter 플래그 사용
|
||||
pnpm --filter as dev
|
||||
```
|
||||
|
||||
### 3.2 `apps/as` 디렉터리 내부에서 실행
|
||||
```bash
|
||||
cd ~/frontend_as/apps/as
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
- **접속 주소**: [http://localhost:3004](http://localhost:3004)
|
||||
- **종료**: 터미널에서 `Ctrl + C`
|
||||
|
||||
---
|
||||
|
||||
## 4. Production (운영 모드) 실행 방법
|
||||
|
||||
반드시 **빌드 산출물(`.next`)이 최신 상태로 생성되어 있어야 정상 실행**됩니다.
|
||||
|
||||
### 4.1 1단계: 프로덕션 빌드 (필수)
|
||||
모노레포 전체를 빌드할 필요 없이 `as` 앱만 단독으로 빌드할 수 있습니다:
|
||||
|
||||
```bash
|
||||
# 프로젝트 루트 디렉터리에서 실행 (약 20~30초 소요)
|
||||
pnpm build:as
|
||||
|
||||
# 또는
|
||||
pnpm --filter as build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.2 2단계: 서버 프로세스 실행
|
||||
|
||||
#### 방법 A: 터미널에서 직접 실행 (로컬 프로덕션 점검용)
|
||||
포그라운드 터미널에서 프로덕션 번들의 빌드 결과와 동작을 직접 테스트할 때 사용합니다:
|
||||
|
||||
```bash
|
||||
# 루트 디렉터리에서 실행
|
||||
pnpm --filter as start
|
||||
|
||||
# 또는 apps/as 디렉터리에서 실행
|
||||
cd apps/as
|
||||
pnpm start
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 방법 B: PM2 프로세스 매니저로 실행 (실서버 운영 표준)
|
||||
실제 운영 서버에서는 `ecosystem.config.js` 설정을 통해 무중단 클러스터(2개 인스턴스)로 실행합니다.
|
||||
|
||||
```bash
|
||||
# 1. 프로세스가 아직 등록되지 않은 경우 (최초 기동)
|
||||
pm2 start ecosystem.config.js --only as2-app
|
||||
|
||||
# 2. 이미 실행 중인 경우 (무중단 롤링 리로드 - 권장)
|
||||
# 순차적으로 인스턴스를 재시작하므로 사용자 요청 끊김이 없습니다.
|
||||
pm2 reload as2-app
|
||||
|
||||
# 3. 프로세스 완전 재시작이 필요한 경우
|
||||
pm2 restart as2-app
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. PM2 운영 및 모니터링 치트시트
|
||||
|
||||
```bash
|
||||
# 1. 전체 프로세스 상태 확인 (상태, 메모리, CPU 점유율 등)
|
||||
pm2 status
|
||||
# 또는
|
||||
pm2 list
|
||||
|
||||
# 2. as2-app 실시간 로그 모니터링
|
||||
pm2 logs as2-app
|
||||
|
||||
# 3. 최근 로그 100줄 출력
|
||||
pm2 logs as2-app --lines 100
|
||||
|
||||
# 4. 프로세스 일시 중지 및 삭제
|
||||
pm2 stop as2-app # 프로세스 일시 중지
|
||||
pm2 delete as2-app # PM2 목록에서 제거
|
||||
|
||||
# 5. 서버 재부팅 시 자동 실행 등록
|
||||
pm2 save # 현재 실행 중인 프로세스 목록 저장
|
||||
pm2 startup # OS 부팅 스크립트 생성
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 실무 배포 요약 워크플로우
|
||||
|
||||
`.env.production` 수정 또는 프론트엔드 코드 수정 후 배포 시 아래 3단계를 수행합니다:
|
||||
|
||||
```bash
|
||||
# Step 1. 최신 코드 반영 (필요시)
|
||||
git pull origin deploy
|
||||
|
||||
# Step 2. as 앱 단독 프로덕션 재빌드
|
||||
pnpm build:as
|
||||
|
||||
# Step 3. PM2 무중단 롤링 리로드
|
||||
pm2 reload as2-app
|
||||
```
|
||||
@@ -0,0 +1,302 @@
|
||||
# 재원협의(fnrs-cnsltn) 플로팅 액션 버튼(FloatingActionButtons) 호출 메커니즘 분석
|
||||
|
||||
## 1. 개요 (Overview)
|
||||
|
||||
본 문서는 **디지털 행정지원(Administrative Support, AS)** 모듈의 **재원협의 상세 조회 화면(`FnrsViewPage`)** 하단에 위치한 [FloatingActionButtons](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx) 컴포넌트의 액션 처리 메커니즘을 상세히 분석한 기술 문서입니다.
|
||||
|
||||
사용자가 화면 하단 액션바에서 **반려(`rjct`) 버튼**을 클릭했을 때 실행되는 `handleRjctClick()` 함수가 내부적으로 중앙 집중식 디스패처인 `executeAction()`과 룩업 테이블(`buttonActionHandlers`)을 거쳐 최종 API 호출 함수인 `handleRejectPrgrsStts()`로 라우팅되는 전 과정을 추적하고, 적용된 아키텍처 디자인 패턴을 설명합니다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 핵심 아키텍처 디자인 패턴
|
||||
|
||||
[FloatingActionButtons](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx)는 수많은 행정 업무 액션(저장, 신청, 삭제, 환경청 제출, 승인, 반려, 검토의견 회신, 보완요청, 기술검토요청 등)을 안전하고 확장 가능하게 관리하기 위해 **전략 패턴(Strategy Pattern)**과 **디스패처(Dispatcher) 패턴**을 결합하여 설계되었습니다.
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ UI 컴포넌트 이벤트 (예: handleRjctClick) │
|
||||
│ - 사용자 Confirm 확인 │
|
||||
│ - 요청 DTO(DocPrgrsUpdateRequestDto) 조립 │
|
||||
└──────────────────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ 중앙 액션 디스패처 (executeAction) │
|
||||
│ - 중복 클릭 방지 (isProcessing 상태 관리) │
|
||||
│ - buttonActionHandlers 룩업 테이블에서 적절한 Strategy 함수 검색 │
|
||||
│ - 일원화된 성공 처리 (Alert 메시지, React Query 캐시 무효화) │
|
||||
│ - 일원화된 에러 처리 (Axios 에러 메시지 추출 및 Alert) │
|
||||
└──────────────────────────────────┬─────────────────────────────────────┘
|
||||
│ action: "rjct" 키로 조회
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ 룩업 테이블 (buttonActionHandlers: Record<Action, Handler>) │
|
||||
│ - rjct: handleRejectPrgrsStts │
|
||||
└──────────────────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ 개별 액션 핸들러 (handleRejectPrgrsStts) │
|
||||
│ - fnrsCnsltnMngService.rejectPrgrsStts(payload) 백엔드 API 호출 │
|
||||
│ - context.onSuccess() / context.onError() 콜백 트리거 │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.1 적용된 핵심 원칙 및 이점
|
||||
1. **단일 책임 원칙 (SRP)**:
|
||||
- 각 UI 이벤트 함수(`handleRjctClick`)는 사용자 확인 및 페이로드 빌드만 담당합니다.
|
||||
- 중앙 디스패처(`executeAction`)는 로딩 상태, 캐시 갱신, 공통 에러 핸들링을 전담합니다.
|
||||
- 실제 비즈니스 통신은 순수 비동기 함수(`handleRejectPrgrsStts`)로 격리됩니다.
|
||||
2. **개방-폐쇄 원칙 (OCP)**:
|
||||
- 신규 액션 버튼이 추가될 경우 `buttonActionHandlers` 레코드에 새로운 키-핸들러 쌍만 등록하면 되며, 공통 에러 처리나 캐시 무효화 로직을 중복 구현할 필요가 없습니다.
|
||||
3. **무결성 및 안정성**:
|
||||
- `setIsProcessing(true)`를 통해 네트워크 요청 진행 중 다중 클릭(Double Submit)을 원천 차단합니다.
|
||||
- 작업 완료 후 `queryClient.invalidateQueries`를 통해 상세 데이터 및 전자결재 분기 상태를 최신 상태로 즉시 동기화합니다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 전체 호출 흐름 시퀀스 다이어그램 (Sequence Diagram)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor User as 사용자
|
||||
participant UI as FloatingActionButtons
|
||||
participant Rjct as handleRjctClick
|
||||
participant Exec as executeAction (디스패처)
|
||||
participant Table as buttonActionHandlers (룩업)
|
||||
participant Handler as handleRejectPrgrsStts
|
||||
participant Service as fnrsCnsltnMngService
|
||||
participant Backend as 백엔드 REST API
|
||||
participant Query as TanStack QueryClient
|
||||
|
||||
User->>UI: [반려] 버튼 클릭
|
||||
UI->>Rjct: handleRjctClick() 트리거
|
||||
Rjct->>User: confirm("해당 신청 건을 반려하시겠습니까?")
|
||||
User-->>Rjct: 확인 (true)
|
||||
|
||||
Note over Rjct: DocPrgrsUpdateRequestDto 생성<br/>{ docNo, docSeComCd, prgrsSttsComCd: "SV", rgtrUuid }
|
||||
|
||||
Rjct->>Exec: executeAction({ action: "rjct", request: payload, successMessage: "반려되었습니다." })
|
||||
|
||||
rect rgb(240, 245, 255)
|
||||
Note over Exec: 1. setIsProcessing(true)<br/>2. const handler = buttonActionHandlers["rjct"]
|
||||
Exec->>Table: buttonActionHandlers["rjct"] 검색
|
||||
Table-->>Exec: handleRejectPrgrsStts 반환
|
||||
end
|
||||
|
||||
Exec->>Handler: handler({ request, data, onSuccess, onError }) 실행
|
||||
|
||||
Handler->>Service: rejectPrgrsStts(context.request) 호출
|
||||
Service->>Backend: PUT /api/v1/as/fnrs-cnsltn-mng/reject-prgrs-stts
|
||||
Backend-->>Service: 200 OK 응답
|
||||
Service-->>Handler: 완료 반환
|
||||
|
||||
Handler->>Exec: context.onSuccess() 콜백 호출
|
||||
|
||||
rect rgb(245, 255, 240)
|
||||
Note over Exec: onSuccess 후속 처리 실행
|
||||
Exec->>User: alert("반려되었습니다.")
|
||||
Exec->>Query: invalidateQueries({ queryKey: fnrsCnsltnKeys.detail(...) })
|
||||
Exec->>Query: invalidateQueries({ queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_BRANCH] })
|
||||
Note over Query: 백엔드 최신 상세 데이터 재조회 및 화면 UI 갱신 (상태: ST -> SV)
|
||||
end
|
||||
|
||||
Note over Exec: finally { setIsProcessing(false) }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 단계별 상세 소스 코드 분석
|
||||
|
||||
### 4.1 Step 1: UI 이벤트 트리거 및 페이로드 조립 (`handleRjctClick`)
|
||||
|
||||
사용자가 화면의 반려 버튼을 클릭하면 [handleRjctClick](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx#L474-L489)이 실행됩니다.
|
||||
|
||||
```typescript
|
||||
// apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx (Line 474~489)
|
||||
async function handleRjctClick() {
|
||||
// 1. 사용자 재확인 대화상자 노출
|
||||
if (!(await confirm("해당 신청 건을 반려하시겠습니까?"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 백엔드 진행상태 업데이트 DTO 빌드 (임시저장 'SV' 상태로 롤백)
|
||||
const payload: DocPrgrsUpdateRequestDto = {
|
||||
docNo, // 대상 문서번호
|
||||
docSeComCd, // 문서구분 공통코드
|
||||
prgrsSttsComCd: "SV", // 변경할 진행상태: 작성중/임시저장(SV)
|
||||
rgtrUuid: userInfo?.userUnqId, // 처리자 고유 식별자
|
||||
};
|
||||
|
||||
// 3. 중앙 디스패처 호출
|
||||
await executeAction({
|
||||
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rjct, // "rjct" 상수
|
||||
request: payload,
|
||||
successMessage: "반려되었습니다.",
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
* **핵심 포인트**: 반려 처리는 문서의 진행 상태를 신청완료(`ST`)에서 작성자(지자체)가 다시 수정할 수 있는 임시저장/작성중(`SV`) 상태로 되돌리는 동작입니다.
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Step 2: 중앙 집중식 액션 디스패처 (`executeAction`)
|
||||
|
||||
[executeAction](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx#L370-L408)은 전달받은 액션 식별자에 따라 룩업 테이블에서 핸들러를 찾아 실행하고, 공통 라이프사이클(로딩, 성공 알림, 쿼리 캐시 무효화, 에러 처리)을 관리합니다.
|
||||
|
||||
```typescript
|
||||
// apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx (Line 370~408)
|
||||
async function executeAction({
|
||||
action,
|
||||
request,
|
||||
successMessage,
|
||||
}: {
|
||||
action: FnrsCnsltnViewButtonAction;
|
||||
request?: unknown;
|
||||
successMessage?: string;
|
||||
}) {
|
||||
// 1. 룩업 테이블에서 액션명에 해당하는 실행 함수 조회
|
||||
const handler = buttonActionHandlers[action];
|
||||
if (!handler) return;
|
||||
|
||||
// 2. 비동기 처리 중 중복 호출 방지 플래그 활성화
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
// 3. 전략 핸들러에 Context 주입 및 실행
|
||||
await handler({
|
||||
request,
|
||||
data,
|
||||
onSuccess: () => {
|
||||
// 성공 메시지 출력
|
||||
if (successMessage) alert(successMessage);
|
||||
|
||||
// 상세 데이터 쿼리 캐시 무효화 -> 화면 자동 갱신
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo),
|
||||
});
|
||||
|
||||
// 전자결재 분기 상태 쿼리 캐시 무효화
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_BRANCH],
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
// 통일된 에러 메시지 추출 및 Alert
|
||||
let errorMessage = "";
|
||||
if (error instanceof AxiosError) {
|
||||
errorMessage = error.response?.data?.message ?? "";
|
||||
} else {
|
||||
errorMessage = error.message ?? "처리 중 오류가 발생했습니다.";
|
||||
}
|
||||
alert(errorMessage);
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
// 4. 로딩 플래그 해제
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.3 Step 3: 전략 매핑 룩업 테이블 (`buttonActionHandlers`)
|
||||
|
||||
[buttonActionHandlers](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx#L1215-L1239)는 컴포넌트 파일 하단에 위치하며, 각 액션 문자열과 실행할 실제 비즈니스 함수를 1:1로 매핑합니다.
|
||||
|
||||
```typescript
|
||||
// apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx (Line 1215~1239)
|
||||
const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> = {
|
||||
strg: handleSaveApply,
|
||||
aply: handleSaveApply,
|
||||
del: handleDel,
|
||||
envoSbmsn: handleChangePrgrsSttsComCd,
|
||||
rjct: handleRejectPrgrsStts, // <--- "rjct" 액션이 handleRejectPrgrsStts 함수로 바인딩됨
|
||||
sncnAprv: handleChangePrgrsSttsComCd,
|
||||
rvwRply: handleTechReview,
|
||||
rvwOpnnRply: handleTechReview,
|
||||
rqstTechRvw: handleRequestExamine,
|
||||
fileSplmnt: handleDmndSplmnt,
|
||||
fileVrfc: () => Promise.resolve(),
|
||||
dsgntRvwr: handleDsgntRvwr,
|
||||
pwrkCnsltnDocCrt: () => Promise.resolve(),
|
||||
pwrkCnsltnDocMng: () => Promise.resolve(),
|
||||
rvwRsltInq: () => Promise.resolve(),
|
||||
rvwStrg: handleTechReview,
|
||||
rvwCmptn: handleTechReview,
|
||||
dmndSplmnt: handleDmndSplmnt,
|
||||
atrzAprv: () => Promise.resolve(),
|
||||
atrzRply: () => Promise.resolve(),
|
||||
actnCmptn: handleActnCmptn,
|
||||
rqstActnRvw: handleRequestActnReview,
|
||||
costVrfc: () => Promise.resolve(),
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Step 4: 실제 비즈니스 API 호출 핸들러 (`handleRejectPrgrsStts`)
|
||||
|
||||
[handleRejectPrgrsStts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx#L1123-L1132) 함수는 전달받은 Context를 언팩하여 백엔드 REST API 서비스를 호출합니다.
|
||||
|
||||
```typescript
|
||||
// apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx (Line 1123~1132)
|
||||
async function handleRejectPrgrsStts(
|
||||
context: ActionHandlerContext<DocPrgrsUpdateRequestDto>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 백엔드 엔티티 서비스 호출
|
||||
await fnrsCnsltnMngService.rejectPrgrsStts(context.request);
|
||||
// 성공 시 executeAction의 onSuccess 실행
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
// 실패 시 executeAction의 onError 실행
|
||||
context.onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 백엔드 서비스 연동부 (`fnrsCnsltnMngService.rejectPrgrsStts`)
|
||||
```typescript
|
||||
// apps/as/src/entities/fnrs-cnsltn/api/fnrs-cnsltn-mng-service.ts
|
||||
async rejectPrgrsStts(body: DocPrgrsUpdateRequestDto): Promise<void> {
|
||||
await this.client.put<CommonResponse<void>>(
|
||||
"/api/v1/as/fnrs-cnsltn-mng/reject-prgrs-stts",
|
||||
body,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 반려 처리 후 화면 갱신 메커니즘 (Post-Action Reaction)
|
||||
|
||||
`handleRejectPrgrsStts`가 성공적으로 종료된 후 화면이 자동으로 갱신되는 과정은 다음과 같습니다:
|
||||
|
||||
1. **콜백 체인 트리거**:
|
||||
`handleRejectPrgrsStts` -> `context.onSuccess()` 실행.
|
||||
2. **Alert 알림 표시**:
|
||||
사용자에게 `"반려되었습니다."` 안내 메시지 팝업 노출.
|
||||
3. **TanStack Query 캐시 무효화 (`invalidateQueries`)**:
|
||||
- `fnrsCnsltnKeys.detail(fnrsCnsltnDocNo)` 키를 가진 React Query 캐시가 Stale 상태로 마킹되어 백엔드로부터 최신 문서 상세 정보를 즉시 다시 조회합니다.
|
||||
- `[ENDPOINT.AS_ELAPR_SERVICE.ELAPR_BRANCH]` 키의 결재 분기 쿼리도 함께 무효화되어 결재선 및 버튼 상태가 재계산됩니다.
|
||||
4. **버튼 가시성 및 권한 자동 재계산**:
|
||||
- 상세 응답 DTO의 `detDto.prgrsSttsComCd`가 `"ST"`(신청완료)에서 `"SV"`(임시저장/작성중)로 변경됩니다.
|
||||
- [button-config.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts)의 권한-상태 매트릭스(`ROLE_STATUS_BUTTON_ACTIONS`)에 따라 화면 하단 버튼이 반려/접수 모드에서 **수정/신청/삭제 모드**로 자동 전환됩니다.
|
||||
- [InstlSncnDetailGate](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/instl-sncn/ui/instl-detail-gate.tsx) 또는 상세 라우팅 규칙에 의해 지자체 작성자는 수정 페이지(`/edit`)로 접근할 수 있게 권한이 활성화됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 요약 및 권장 개발 가이드라인
|
||||
|
||||
| 항목 | 구현 내용 | 아키텍처적 가치 |
|
||||
| :--- | :--- | :--- |
|
||||
| **액션 트리거** | `handleRjctClick` | 사용자 의사 확인(`confirm`) 및 DTO 캡슐화 |
|
||||
| **공통 처리** | `executeAction` | 로딩 상태(`isProcessing`), 에러 Alert, 캐시 무효화 일원화 |
|
||||
| **동적 디스패치** | `buttonActionHandlers[action]` | 조건문(if-else/switch) 없는 깔끔한 O(1) 룩업 테이블 |
|
||||
| **API 실행** | `handleRejectPrgrsStts` | 도메인 엔티티 서비스(`fnrsCnsltnMngService`)에 통신 위임 |
|
||||
| **화면 동기화** | `invalidateQueries` | 수동 상태 변이 없이 선언적 서버 상태 재동기화 |
|
||||
|
||||
다른 업무 모듈(`instl-sncn`, `pwrk-cnsltn`, `design-cnstn` 등) 개발 시에도 본 메커니즘과 동일하게 **`[ActionHandler] -> [buttonActionHandlers] -> [executeAction]`** 구조를 준수하여 구현하면 일관되고 견고한 엔터프라이즈 UI를 유지할 수 있습니다.
|
||||
@@ -0,0 +1,183 @@
|
||||
# 병합자 실무협의서(MAIN) 웹기안기 탭 전환 시 문서 초기화 이슈 및 해결방안
|
||||
|
||||
> **문서 대상**: `apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx`
|
||||
> **관련 모듈**: 재원협의·실무협의(`fnrs-cnsltn` / `pwrk-cnsltn`), 웹한글기안기(`@/widgets/hangulgiangi`), 전자결재(`elapr`)
|
||||
> **작성일**: 2026-10-23
|
||||
> **상태**: 구현 완료 (`PwrkCnsltnBranchRefPanel` 사이드 패널 Split View 적용 완료)
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요 (Overview)
|
||||
|
||||
재원협의(FNRS)의 하위 실무협의(PWRK) 프로세스 중 병렬 결재(Fork-Join, `TE`/`RK` 상태) 단계에서, **종합병합자(TOTAL_AGG 또는 ATRZ04 권한자)**는 관로(`SP`)와 시설(`SF`) 부서의 초안 및 검토 의견을 종합하여 최종 종합 실무협의서인 **`MAIN` 탭 문서를 편집·반영하고 저장**하는 법적·업무적 책임을 집니다.
|
||||
|
||||
현재 구현된 [PwrkCnsltnDocMngDialog](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx)에서는 병합자가 `MAIN` 문서를 수정하던 중 부서별 작성 내용을 확인하기 위해 `[관로 초안 (SP)]` 또는 `[시설 초안 (SF)]` 탭 버튼을 클릭하고 다시 `[종합 실무협의서 (MAIN)]` 탭으로 돌아왔을 때, **기안기 내부에서 편집 중이던 수정 내용이 모두 사라지고 서버에 저장된 원래 버전으로 초기화(Overwrite)되는 치명적인 데이터 유실 문제**가 발생하고 있습니다.
|
||||
|
||||
본 문서는 해당 현상의 근본 기술 원인을 분석하고, 병합자의 업무 흐름과 시스템 제약사항을 만족하는 실질적 해결 방안을 검토·제시합니다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 현상 및 업무적 배경 (Business Context & Issue)
|
||||
|
||||
### 2.1 병합자의 역할 및 책임
|
||||
1. **수정 및 저장의 단일 주체**:
|
||||
- 병렬 결재(`isForkJoin === true`) 환경에서 관로(`SP`) 및 시설(`SF`) 부서의 1차 검토가 완료되면 병합자가 전체 내용을 종합합니다.
|
||||
- 병합자는 오직 **`MAIN` 종합본만 수정·저장**할 수 있으며, `SP`와 `SF` 문서는 수정 권한이 없고 **오직 참조(열람)만 가능**합니다 (`canSaveCurrentBranch` 조건 참조).
|
||||
2. **반영 및 대조(Cross-Check) 업무**:
|
||||
- 병합자는 `SP`의 검토 의견/사업비 조정 결과와 `SF`의 검토 의견/사업비 조정 결과를 문단별·항목별로 대조하며 `MAIN` 문서의 본문과 표를 직접 수기 편집(병합)해야 합니다.
|
||||
|
||||
### 2.2 발생 현상 (Problem Scenario)
|
||||
```
|
||||
[사용자 행동 흐름]
|
||||
1. 병합자가 다이얼로그 진입 시 기본 활성화된 [종합 실무협의서 (MAIN)] 탭에서 한글 문서 수정 시작
|
||||
2. 관로 부서가 수정한 상세 내역을 확인하기 위해 상단 [관로 초안 (SP)] 탭 버튼 클릭
|
||||
3. 관로 초안 확인 후, 다시 상단 [종합 실무협의서 (MAIN)] 탭 버튼 클릭
|
||||
4. 결과: 방금 전까지 작성/수정 중이던 MAIN 한글 문서 내용이 전부 사라지고 서버 원본으로 롤백됨
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 기술적 원인 분석 (Root Cause Analysis)
|
||||
|
||||
### 3.1 단일 웹한글기안기(iframe) 인스턴스 공유 및 무조건 문서 로드 (`HwpCtrl.Open`)
|
||||
[PwrkCnsltnDocMngDialog.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx#L357-L383)의 구조:
|
||||
```tsx
|
||||
// 탭 변경 시 targetBranchForApi 변경 -> React Query가 해당 탭의 최신 HWPX Blob 재조회
|
||||
const targetBranchForApi = isForkJoin ? currentBranchSeCd : undefined;
|
||||
const { data: documentBlob } = useQuery({
|
||||
queryKey: ["pwrk-cnsltn-doc-crt-rslt", ..., targetBranchForApi, ...],
|
||||
queryFn: () => pwrkCnsltnDocCrtRsltService.openWebHwpLatest(crtDmndUuid!, targetBranchForApi),
|
||||
});
|
||||
```
|
||||
|
||||
기안기 렌더링부:
|
||||
```tsx
|
||||
<HangulGiangi
|
||||
documentBlob={documentBlob}
|
||||
documentKey={`${completedDraft.crtDmndUuid}_${currentBranchSeCd}_${selectedVerSn ?? "latest"}`}
|
||||
...
|
||||
/>
|
||||
```
|
||||
|
||||
[HangulGiangi.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx#L120-L130)의 문서 로드 이펙트:
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
if (!isCtrlReady || ...) return;
|
||||
// documentKey가 변경되면 기존 기안기 컨트롤에 새 Blob을 바로 Open해버림!
|
||||
openHwpxBlob(documentBlob, documentFileName);
|
||||
}, [documentBlob, documentFileName, documentKey, isCtrlReady, openHwpxBlob]);
|
||||
```
|
||||
|
||||
- 사용자가 탭을 전환하면 `currentBranchSeCd`가 바뀌면서 `documentKey`가 달라집니다.
|
||||
- 단일 iframe에 위치한 기안기 컨트롤러(`HwpCtrl`)는 즉시 `HwpCtrl.Open(documentFile)`을 실행하여 새로운 HWPX 문서를 캔버스에 로드합니다.
|
||||
- 이 과정에서 기안기 메모리 버퍼에 존재하던 `MAIN` 문서의 편집 변경사항은 **서버 저장이나 로컬 캐싱 없이 즉시 파기**됩니다.
|
||||
- 다시 `MAIN` 탭을 클릭하면 서버의 최신 저장본(`openWebHwpLatest`)이 다시 `Open`되므로, 작성 중이던 내용이 완전히 날아가게 됩니다.
|
||||
|
||||
### 3.2 변경 감지(Dirty Check) 및 화면 이탈 가드 부재
|
||||
- 한컴 웹기안기는 문서 수정 여부를 판별할 수 있는 프로퍼티(`HwpCtrl.IsModified`)를 제공하지만([ref-webhwp-api.md:L269](file:///Users/changwng/workspace.ona/frontend_as/apps/as/docs/pwrk-cnsltn/ref-webhwp-api.md#L269)), 탭 전환 이벤트 핸들러에서 이를 전혀 검사하지 않고 즉시 state를 변경합니다.
|
||||
- 미저장 변경사항이 있을 때 사용자에게 확인을 요청하는 컨펌/모달 가드가 없습니다.
|
||||
|
||||
### 3.3 UX 설계와 병합자 업무 특성의 불일치
|
||||
- 병합자는 `SP`/`SF`를 보면서 **동시에 `MAIN`에 입력·복사**해야 합니다.
|
||||
- 화면 전체를 `SP` $\leftrightarrow$ `MAIN`으로 번갈아 스위칭하는 단일 캔버스 탭 방식은 초기화 문제 외에도 **"앞서 본 내용을 외우거나 메모장에 복사한 뒤 탭을 넘나들어야 하는" 극심한 업무 비효율**을 초래합니다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 해결 방안 비교 검토 (Solution Analysis)
|
||||
|
||||
| 평가 항목 | [방안 1] 병합자용 참조 문서 분할/별도 창 뷰어 제공 (강력 권장 ★★★★★) | [방안 2] 탭 전환 시 인메모리 Blob 임시 캐싱 | [방안 3] 탭 전환 시 Dirty Check 및 저장 강제 컨펌 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **개념** | `MAIN` 기안기는 항상 화면에 유지하고, `SP`/`SF`는 **별도 새 창(Window) 또는 팝업 모달/우측 패널**로 열람 | 탭 전환 시 `HwpCtrl.SaveAs`로 HWPX Blob을 추출해 React State/Ref에 임시 보관 후 복귀 시 재오픈 | 미저장 변경사항(`IsModified`)이 있을 때 탭 전환을 차단하고 서버에 먼저 저장하도록 유도 |
|
||||
| **초기화 방지** | **원천 차단** (`MAIN` 기안기 인스턴스가 절대 언로드되지 않음) | 해결 가능 (메모리에서 복구) | 해결 가능 (저장 후 이동) |
|
||||
| **병합 업무 편의성** | **최상** (듀얼 모니터 또는 분할 창으로 두 문서를 나란히 대조 가능) | 보통 (여전히 탭을 왔다 갔다 하며 외워서 작업해야 함) | 낮음 (SP 한번 보려면 MAIN 서버 저장 필수, 버전 폭증) |
|
||||
| **시스템 안정성** | **매우 높음** (복잡한 기안기 상태 관리 불필요) | 낮음 ~ 보통 (비동기 콜백 레이스 컨디션 및 메모리 누수 위험) | 보통 (불필요한 `verSn` 이력 증가) |
|
||||
| **구현 복잡도** | 낮음 ~ 보통 | 높음 | 낮음 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 상세 권장 해결책 (Recommended Architecture)
|
||||
|
||||
가장 안전하고 병합자의 업무 생산성을 극대화할 수 있는 **하이브리드 접근법(단계별 적용)**을 권장합니다.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph 병합자(TOTAL_AGG) 화면 구조
|
||||
MAIN_VIEW["MAIN 기안기 (항상 유지, 수정 내용 보존)"]
|
||||
BTN_SP["[관로(SP) 초안 새 창 열기 / 대조]"]
|
||||
BTN_SF["[시설(SF) 초안 새 창 열기 / 대조]"]
|
||||
GUARD{"탭 전환 시도 시<br/>HwpCtrl.IsModified 체크"}
|
||||
end
|
||||
|
||||
subgraph 별도 참조 뷰 (Popup / Window / Split)
|
||||
REF_SP["관로(SP) 읽기 전용 뷰어"]
|
||||
REF_SF["시설(SF) 읽기 전용 뷰어"]
|
||||
end
|
||||
|
||||
BTN_SP -->|독립 실행| REF_SP
|
||||
BTN_SF -->|독립 실행| REF_SF
|
||||
REF_SP -.동시 대조 및 MAIN에 복사/반영.-> MAIN_VIEW
|
||||
REF_SF -.동시 대조 및 MAIN에 복사/반영.-> MAIN_VIEW
|
||||
GUARD -->|미저장 내용 있음| WARN["경고 알림: 저장 후 이동 또는 취소"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 단계 1: 병합자를 위한 "SP / SF 참조 문서 별도 창 열람" 제공 (방안 1 적용 완료)
|
||||
|
||||
병합자에게 `SP`와 `SF`는 편집 대상이 아니라 **참조(열람) 대상**이므로, `MAIN` 기안기 화면을 전환하지 않고 별도 창(Popup Window)으로 띄워 듀얼 모니터나 분할 화면으로 대조할 수 있도록 구현했습니다.
|
||||
|
||||
#### 1) UI 액션 버튼 분기
|
||||
병합자(`isMerger === true`)가 `MAIN` 탭을 보고 있을 때, 상단 탭 영역 우측에 **독립 새 창 열기 버튼**을 배치:
|
||||
- `[관로(SP) 새 창 보기]`
|
||||
- `[시설(SF) 새 창 보기]`
|
||||
|
||||
#### 2) 새 창 열기 및 한컴 라이선스 오류 해결 메커니즘
|
||||
- **라이선스 오류 원인 및 해결**:
|
||||
- 기존 새 창 iframe 호출 시 `/webhwpctrl/index.html`로만 호출하여 `baseurl` 쿼리 파라미터가 누락되었고, 로컬 주소(`localhost:3004`)를 컨트롤 기본 URL로 인식하여 한컴 기안기 스크립트에서 `라이센스 오류` alert이 발생함.
|
||||
- 이를 해결하기 위해 `buildBridgeSrc(DEFAULT_BRIDGE_URL, WEBHYP_CTRL_URL)` 유틸 함수를 통해 `?baseurl=https://hwp.onasys.co.kr/webhwpctrl`이 안전하게 주입되도록 개선.
|
||||
- **HWPX 다운로드 Fallback 지원**:
|
||||
- 새 창 상단 헤더에 `[HWPX 다운로드]` 버튼을 함께 제공하여, 웹기안기 뷰어 외에도 로컬 한컴오피스 프로그램으로 즉시 문서를 내려받아 열람할 수 있도록 이중 보장.
|
||||
|
||||
---
|
||||
|
||||
### 단계 2: 탭 전환 시 Dirty Check 및 데이터 유실 방지 가드 (안전 가드 적용 완료)
|
||||
|
||||
만약 사용자가 기존 탭 버튼(`SP`, `SF`)을 눌러 탭을 전환하려고 시도할 때, 작성 중이던 내용이 소실되지 않도록 확인 가드를 배치했습니다.
|
||||
|
||||
#### 1) `HwpCtrl.IsModified` 감지 함수
|
||||
- `apps/as/src/widgets/hangulgiangi/lib/get-hwp-ctrl.ts`의 `getHwpCtrl(null)`을 통해 현재 활성화된 한컴 기안기 컨트롤의 `IsModified` 플래그를 실시간 확인.
|
||||
|
||||
#### 2) 탭 전환 및 창 닫기 안전 확인
|
||||
```typescript
|
||||
function checkDirtyAndConfirm(targetDesc: string): boolean {
|
||||
if (currentBranchSeCd === "MAIN") {
|
||||
try {
|
||||
const ctrl = getHwpCtrl(null);
|
||||
if (ctrl?.IsModified) {
|
||||
return window.confirm(
|
||||
`종합 실무협의서(MAIN)에 아직 저장되지 않은 수정 내용이 있습니다.\n` +
|
||||
`${targetDesc}(으)로 이동하면 작성 중인 내용이 초기화됩니다.\n\n` +
|
||||
`계속 진행하시겠습니까? (취소 후 [실무협의서 저장]을 권장합니다.)`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("기안기 변경 감지 실패:", e);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 결론 및 적용 결과
|
||||
|
||||
1. **초기화 버그 원천 해결**:
|
||||
- `MAIN` 탭을 벗어나지 않고도 `[관로(SP) 새 창 보기]`, `[시설(SF) 새 창 보기]`를 통해 독립 팝업으로 각 부서의 원본 문서를 대조할 수 있어 `MAIN` 기안기 인스턴스가 절대 언로드되지 않습니다.
|
||||
- 실수로 탭을 누르거나 창을 닫으려 할 때도 `IsModified` 기반 안전 컨펌 창이 작동하여 데이터 유실을 100% 방지합니다.
|
||||
2. **새 창 웹기안기 라이선스 인증 정상화**:
|
||||
- 새 창 iframe의 브리지 URL에 `WEBHYP_CTRL_URL` 파라미터를 정상 바인딩하여 "라이센스 오류" alert을 근본적으로 제거하였습니다.
|
||||
- 새 창 헤더에 `[HWPX 다운로드]` 버튼을 추가하여 로컬 환경에서도 편리하게 문서를 열어볼 수 있도록 지원합니다.
|
||||
3. **단위 테스트 검증 완료**:
|
||||
- 총 19개 단위 테스트(`pnpm --filter as exec vitest run src/pages/fnrs-cnsltn/ui/__tests__/pwrk-cnsltn-doc-mng-dialog.test.tsx`)가 전체 통과(`19 passed (19)`)함을 검증했습니다.
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
# TanStack Query 상세 데이터 캐시 동기화 이슈 및 해결방안
|
||||
|
||||
> **문서 대상**: `apps/as/src/entities/fnrs-cnsltn/api/queries.ts`, `apps/as/src/shared/lib/query-client.ts`
|
||||
> **관련 모듈**: 재원협의(`fnrs-cnsltn`), 실무협의(`pwrk-cnsltn`), 웹한글기안기(`@/widgets/hangulgiangi`), 인허가 공통(`instl-sncn`, `design-cnstn` 등)
|
||||
> **작성일**: 2026-10-23
|
||||
> **상태**: 분석 및 전체 코드 적용 완료 (검증 완료)
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요 (Overview)
|
||||
|
||||
재원협의(`fnrs-cnsltn`) 및 인허가 업무 프로세스는 지자체 담당자(신청자), 한국환경공단 담당자(검토자), 결재권자 등 **다수의 사용자가 실시간으로 문서를 조회·작성·보완·결재하는 다자간 협업 환경**입니다.
|
||||
|
||||
최근 운영 및 테스트 환경에서 **"목록 리스트에서 상세 화면으로 진입했을 때, 다른 사용자가 직전에 입력/수정한 최신 데이터가 바로 조회되지 않고, 브라우저 새로고침(F5, 새로 읽기)을 해야만 화면에 정상 표시되는 현상"**이 보고되었습니다.
|
||||
|
||||
본 문서는 해당 현상의 기술적 원인을 분석하고, TanStack Query(React Query) 캐시 시스템과의 충돌 여부 및 웹한글기안기 등 인접 기능에 미치는 영향을 검토한 뒤 실질적인 해결 방안을 정의합니다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 현상 및 재현 시나리오 (Problem Scenario)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor UserA as 사용자 A (공단 검토자)
|
||||
actor UserB as 사용자 B (지자체 신청자)
|
||||
participant ClientA as 사용자 A 브라우저 (React Query)
|
||||
participant Server as 백엔드 API (Spring Boot / DB)
|
||||
|
||||
Note over UserA, ClientA: 1. 문서 1번 상세 조회 (최초 진입)
|
||||
UserA->>ClientA: 목록에서 문서 1번 클릭
|
||||
ClientA->>Server: GET /server/as/v1/fnrs-cnsltn-mng/1
|
||||
Server-->>ClientA: 200 OK (구 데이터 반환)
|
||||
ClientA->>ClientA: React Query 캐시에 저장 (staleTime: 60초 시작)
|
||||
UserA->>ClientA: 목록 화면으로 이동 (SPA 클라이언트 라우팅)
|
||||
|
||||
Note over UserB, Server: 2. 타 사용자의 데이터 수정 및 저장
|
||||
UserB->>Server: POST /server/as/v1/fnrs-cnsltn-mng (데이터 보완/수정 완료)
|
||||
Server-->>UserB: 200 OK (DB 최신화 완료)
|
||||
|
||||
Note over UserA, ClientA: 3. 60초 이내에 문서 1번 상세 재진입 (문제 발생!)
|
||||
UserA->>ClientA: 목록에서 문서 1번 다시 클릭
|
||||
Note over ClientA: ⚠️ staleTime(60초) 미경과로 캐시를 'fresh' 상태로 판단<br/>→ 서버 API 요청 없이 기존 구 캐시 데이터 반환!
|
||||
ClientA-->>UserA: 이전 구 데이터 화면 표시 (수정 내용 미반영)
|
||||
|
||||
Note over UserA, ClientA: 4. 브라우저 새로고침(F5 / 새로 읽기)
|
||||
UserA->>ClientA: 브라우저 새로고침 (F5)
|
||||
Note over ClientA: JS 런타임 메모리 초기화 → 캐시 삭제
|
||||
ClientA->>Server: GET /server/as/v1/fnrs-cnsltn-mng/1
|
||||
Server-->>ClientA: 200 OK (최신 데이터 반환)
|
||||
ClientA-->>UserA: 최신 데이터 정상 표시!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 기술적 근본 원인 분석 (Root Cause Analysis)
|
||||
|
||||
### 3.1 [핵심 원인] 전역 `staleTime: 60초` 캐싱 및 네트워크 요청 생략
|
||||
- **설정 파일**: [query-client.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/shared/lib/query-client.ts#L3-L11)
|
||||
```typescript
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 60 * 1000, // 👈 60초(1분) 동안 캐시를 "fresh(신선한)" 상태로 간주
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
- **상세 쿼리**: [queries.ts: useFnrsCnsltnMng](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/entities/fnrs-cnsltn/api/queries.ts#L33-L39)
|
||||
```typescript
|
||||
export function useFnrsCnsltnMng(fnrsCnsltnDocNo: string | null | undefined) {
|
||||
return useQuery({
|
||||
queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo!),
|
||||
queryFn: () => fnrsCnsltnMngService.findById(fnrsCnsltnDocNo!),
|
||||
enabled: Boolean(fnrsCnsltnDocNo),
|
||||
// ⚠️ staleTime이 지정되지 않아 전역 기본값인 60초가 그대로 상속됨
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### 동작 메커니즘
|
||||
1. TanStack Query는 데이터가 한 번 패칭되면 `staleTime`(60초) 동안 데이터를 `fresh` 상태로 취급합니다.
|
||||
2. 목록 화면에서 상세 화면으로 이동할 때(`router.push()`) 컴포넌트가 다시 마운트되더라도, 데이터가 `fresh` 상태이면 **네트워크 요청(refetch)을 일절 수행하지 않고 메모리 캐시를 그대로 반환**합니다.
|
||||
3. 본인이 직접 저장/수정(`mutation`)을 실행한 경우에는 `queryClient.invalidateQueries`가 호출되어 본인 브라우저 캐시는 무효화되지만, **다른 사용자의 브라우저에는 어떠한 무효화 신호도 전달되지 않습니다.**
|
||||
4. 따라서 다른 사용자가 실시간으로 문서를 변경하더라도 60초 동안은 무조건 구 데이터가 표시됩니다.
|
||||
5. 브라우저 새로고침(F5)을 누르면 SPA 런타임 메모리가 초기화되어 React Query 캐시가 사라지므로, 네트워크 요청을 새로 보내 최신 데이터가 조회되는 것입니다.
|
||||
|
||||
---
|
||||
|
||||
### 3.2 [부차 원인] 브라우저 HTTP GET 캐싱 (Cache-Control 부재)
|
||||
- [packages/common/network/client.ts](file:///Users/changwng/workspace.ona/frontend_as/packages/common/network/client.ts#L20-L27)의 Axios 인스턴스 기본 요청 헤더에 `Cache-Control: no-cache`가 설정되어 있지 않습니다.
|
||||
- 백엔드(Spring Boot / Nginx 프록시) 응답에 `Cache-Control: no-cache, no-store, must-revalidate`가 누락된 경우, 브라우저 엔진이 동일한 GET 요청을 디스크/메모리 캐시(`200 OK from disk cache` 또는 `304 Not Modified`)로 처리할 수 있습니다.
|
||||
- 브라우저 새로고침 시에는 브라우저가 강제로 `Cache-Control: max-age=0`을 포함하여 요청하므로 최신 데이터를 가져오게 됩니다.
|
||||
|
||||
---
|
||||
|
||||
### 3.3 [파생 이슈] 상세 게이트(`FnrsDetailGate`) 및 폼 페이지 영향
|
||||
1. **게이트 권한 판별 오작동**:
|
||||
- [fnrs-detail-gate.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-detail-gate.tsx#L16-L21)는 상세 데이터의 진행상태(`prgrsSttsComCd`)를 기반으로 수정 권한(`canEditPbadmsDoc`)을 판별하여 `/edit` 페이지로 보낼지, 뷰 페이지로 보낼지 결정합니다.
|
||||
- 캐시로 인해 다른 사용자가 문서를 제출(`ST`)했음에도 이전 상태(임시저장 `SV`)로 남아있으면, 잘못된 페이지로 분기될 위험이 발생합니다.
|
||||
2. **React Hook Form 초기화 바인딩 문제**:
|
||||
- 수정 모드([fnrs-form-page.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx#L90-L97))에서는 `initialData`로 `useForm({ defaultValues: ... })`를 마운트 시점에 1회 초기화합니다.
|
||||
- 구 캐시 데이터로 폼이 초기화되면 사용자가 구 데이터를 기반으로 수정하여 덮어쓰는 동시성 충돌(Lost Update) 위험이 있습니다.
|
||||
|
||||
---
|
||||
|
||||
## 4. TanStack Query 캐시 기능과의 충돌 여부 검토
|
||||
|
||||
### 4.1 기본 캐시 메커니즘(`staleTime` vs `gcTime`)과의 충돌 여부
|
||||
|
||||
> **결론**: 상세 쿼리에 `staleTime: 0`을 설정하더라도 **TanStack Query의 캐시 기능과 전혀 충돌하지 않으며, TanStack Query 고유의 SWR(Stale-While-Revalidate) 아키텍처에 완벽히 부합**합니다.
|
||||
|
||||
- `staleTime`: 데이터가 신선하다고 간주되는 시간 $\rightarrow$ `0`으로 두면 진입 시 항상 최신성 검증(재요청).
|
||||
- `gcTime`(구 cacheTime): 사용되지 않는 캐시를 메모리에 유지하는 시간 $\rightarrow$ 기본 `5분` 유지.
|
||||
- **결과**: `staleTime: 0`이라 하더라도 기존 캐시가 메모리에 존재하므로, 화면 진입 시 **이전 데이터를 먼저 즉시 보여주어 화면 깜빡임(Flicker/Skeleton)을 방지**하고, 백그라운드에서 최신 데이터를 가져와 부드럽게 화면을 갱신합니다.
|
||||
|
||||
---
|
||||
|
||||
### 4.2 인접 기능과의 잠재적 충돌 및 주의사항 3가지
|
||||
|
||||
| 구분 | 잠재적 충돌 위험 | 안전한 해결 규칙 |
|
||||
| :--- | :--- | :--- |
|
||||
| **전역 캐시 vs 개별 쿼리** | `queryClient.ts`의 전역 기본값을 `staleTime: 0`으로 변경 시 공통코드, 조직도, 회계트리 등 마스터 데이터 요청이 폭증함 | 전역 설정은 유지하고, **업무 상세 쿼리(`useFnrsCnsltnMng` 등)에만 국소적으로 `staleTime: 0` 부여** |
|
||||
| **웹한글기안기 (HWPX Blob)** | 기안기 Blob 쿼리에 `staleTime: 0` 또는 `refetchOnWindowFocus: true`가 걸리면, 다른 탭을 다녀올 때 Blob이 재조회되어 **편집 중이던 기안기 캔버스가 서버 원본으로 덮어써짐(데이터 유실)** | **웹기안기 Blob 쿼리는 자동 refetch 대상에서 제외**, 오직 저장 성공 시(`invalidateQueries`)에만 갱신 (분리 원칙) |
|
||||
| **수정 폼 (React Hook Form)** | 캐시 데이터로 `defaultValues`가 바인딩된 후 백그라운드 fetch가 완료되어도 RHF는 입력값 보호를 위해 자동 갱신하지 않음 | 수정 페이지 진입 시 최신 데이터 로딩 완료를 보장하거나, 뷰 페이지 위주로 `staleTime: 0`을 적극 활용 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 단계별 해결 방안 (Recommended Solutions)
|
||||
|
||||
### 단계 1: 상세 조회 쿼리에 도메인 캐시 정책 명시 (필수 적용 ★★★★★)
|
||||
|
||||
[apps/as/src/entities/fnrs-cnsltn/api/queries.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/entities/fnrs-cnsltn/api/queries.ts)의 `useFnrsCnsltnMng`에 업무 특성에 맞춘 옵션을 선언합니다.
|
||||
|
||||
```typescript
|
||||
export function useFnrsCnsltnMng(fnrsCnsltnDocNo: string | null | undefined) {
|
||||
return useQuery({
|
||||
queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo!),
|
||||
queryFn: () => fnrsCnsltnMngService.findById(fnrsCnsltnDocNo!),
|
||||
enabled: Boolean(fnrsCnsltnDocNo),
|
||||
staleTime: 0, // 상세 화면 진입 시 항상 최신 데이터 조회 (fresh 상태 유지 해제)
|
||||
refetchOnMount: "always", // 마운트 시 항상 백그라운드 재조회 수행
|
||||
refetchOnWindowFocus: false, // 웹기안기 팝업이나 외부 창 이동 중 캔버스 리로드 방지를 위해 false 유지
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 단계 2: 클라이언트 Axios HTTP 캐시 방지 헤더 보강 (안전장치 적용)
|
||||
|
||||
브라우저 레벨의 HTTP 디스크/메모리 캐싱을 원천 방지하기 위해 [packages/common/network/client.ts](file:///Users/changwng/workspace.ona/frontend_as/packages/common/network/client.ts)에 캐시 제어 헤더를 주입합니다.
|
||||
|
||||
```typescript
|
||||
// packages/common/network/client.ts
|
||||
this.axiosInstance = axios.create({
|
||||
baseURL: config.baseURL,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": config.siteId,
|
||||
"Cache-Control": "no-cache",
|
||||
"Pragma": "no-cache",
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 단계 3: 상세 화면 상단에 '새로고침(동기화)' UX 제공 (선택 권장)
|
||||
|
||||
전화나 메신저로 소통하며 실시간 결재/보완 작업을 진행하는 공단-지자체 업무 특성을 고려하여, 브라우저 전체(F5)를 새로고침하지 않고도 상세 데이터만 즉시 동기화할 수 있는 **동기화 버튼**을 상단 바에 배치합니다.
|
||||
|
||||
```tsx
|
||||
// apps/as/src/pages/fnrs-cnsltn/ui/fnrs-view-page.tsx
|
||||
const { data, refetch, isFetching } = useFnrsCnsltnMng(id);
|
||||
|
||||
// SectionCollapsibleToggle 옆에 배치:
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => refetch()}
|
||||
disabled={isFetching}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<RefreshCw className={cn("size-4", isFetching && "animate-spin")} />
|
||||
<span>새로고침</span>
|
||||
</Button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 실제 코드 적용 내역 (Applied Implementation)
|
||||
|
||||
분석된 내용을 바탕으로 프로젝트 코드에 다음 사항들이 100% 적용되었습니다.
|
||||
|
||||
### 5.1 재원협의(`fnrs-cnsltn`) 상세 쿼리 캐시 정책 적용
|
||||
- **적용 파일**: [apps/as/src/entities/fnrs-cnsltn/api/queries.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/entities/fnrs-cnsltn/api/queries.ts)
|
||||
- `useFnrsCnsltnMng`, `useFnrsCnsltnFileVrfcResults`에 `staleTime: 0`, `refetchOnMount: "always"` 옵션 주입 완료.
|
||||
|
||||
### 5.2 타 인허가 모듈 확산 적용
|
||||
1. **설치인가(`instl-sncn`)**:
|
||||
- [apps/as/src/entities/instl-sncn/api/queries.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/entities/instl-sncn/api/queries.ts): `useInstlSncnDetail`에 `staleTime: 0`, `refetchOnMount: "always"` 적용 완료.
|
||||
2. **설계검토(`design-cnstn`)**:
|
||||
- [apps/as/src/entities/design-cnstn/api/queries.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/entities/design-cnstn/api/queries.ts): `useDesignCnstnMng`에 `staleTime: 0`, `refetchOnMount: "always"` 적용 완료.
|
||||
3. **기본계획(`sewer-bsc-plan`)**:
|
||||
- [apps/as/src/entities/sewer-bsc-plan/api/queries.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/entities/sewer-bsc-plan/api/queries.ts): `useSewerBscPlan`, `useSewerBscPlanDetails`에 `staleTime: 0`, `refetchOnMount: "always"` 적용 완료.
|
||||
|
||||
### 5.3 클라이언트 Axios HTTP 캐시 방지 헤더 주입
|
||||
- **적용 파일**: [packages/common/network/client.ts](file:///Users/changwng/workspace.ona/frontend_as/packages/common/network/client.ts)
|
||||
- `axios.create`의 기본 헤더에 `Cache-Control: "no-cache"`, `Pragma: "no-cache"` 주입 완료.
|
||||
|
||||
### 5.4 전체 인허가 상세 조회 화면 상단 '새로 고침' 버튼 UX 적용
|
||||
상단 `sticky` 헤더 영역에 `refetch()`를 트리거하는 `[새로 고침]` 버튼 및 로딩 회전 애니메이션(`animate-spin`)을 일괄 적용 완료했습니다.
|
||||
1. **재원협의(`fnrs-cnsltn`)**: [apps/as/src/pages/fnrs-cnsltn/ui/fnrs-view-page.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-view-page.tsx)
|
||||
2. **설치인가(`instl-sncn`)**: [apps/as/src/pages/instl-sncn/ui/instl-view-page.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/instl-sncn/ui/instl-view-page.tsx)
|
||||
3. **설계검토(`design-cnstn`)**: [apps/as/src/pages/design-cnstn/ui/design-view-page.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/design-cnstn/ui/design-view-page.tsx)
|
||||
4. **기본계획(`sewer-bsc-plan`)**: [apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-view-page.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-view-page.tsx)
|
||||
|
||||
---
|
||||
|
||||
## 6. 검증 및 결과
|
||||
|
||||
1. **TypeScript 컴파일 검증**:
|
||||
- `pnpm --filter as exec tsc --noEmit` $\rightarrow$ 에러 없이 100% 정상 통과.
|
||||
- `pnpm --filter @common/network exec tsc --noEmit` $\rightarrow$ 에러 없이 100% 정상 통과.
|
||||
2. **ESLint 정적 검사 통과**:
|
||||
- `pnpm --filter as lint` 및 `eslint --fix` 적용 완료.
|
||||
3. **적용 효과**:
|
||||
- 사용자가 목록에서 상세로 진입할 때마다 서버로부터 항상 최신 데이터를 재조회하여 화면에 반영.
|
||||
- 이전 캐시가 존재할 경우 SWR 패턴에 따라 깜빡임 없이 즉시 렌더링된 후 백그라운드 최신화 완료.
|
||||
- 웹한글기안기 HWPX Blob 쿼리는 영향받지 않아 캔버스 덮어쓰기 위험 원천 방지.
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
# 재원협의·실무협의 프론트엔드 개발 가이드
|
||||
|
||||
> **대상 앱**: `frontend/apps/as`
|
||||
> **작성일**: 2026-08-27
|
||||
> **개정일**: 2026-09-07
|
||||
> **버전**: 2.0
|
||||
> **Process**: PR-AS-003 (재원협의 관리 — 실무협의 및 AI 검증 통합)
|
||||
>
|
||||
> **SoT (2026-09-03 완결)**:
|
||||
>
|
||||
> - 프로세스·상태·버튼: `03.재원협의_실무협의_프로세스_통합_v완결.md`
|
||||
> - REST 설계: `05.재원협의_상태별_endpoint_설계.md`
|
||||
> - 공통코드: `FNRSSTTS` (`SV, ST, LE, FV, ER, SL, AC, TE, TP, RM, RB, RC, RA, AV, EV, RK, RR, EP, TA`)
|
||||
>
|
||||
> **백엔드 서비스**:
|
||||
>
|
||||
> - `as-service` — `/api/v1/fnrs-cnsltn-mng`, `/api/v1/pwrk-cnsltn-mng`, `/api/v1/pwrk-cnsltn-doc-crt-rslt`, `/api/v1/dcrag-to`
|
||||
> - `as-elapr-service` — `/api/v1/elapr-dmnd`, `/api/v1/elapr-prgrs-stts`, `/api/v1/elapr-apln`
|
||||
>
|
||||
> **현재 프론트 상태**: 재원협의(`fnrs-cnsltn`)·전자결재(`elapr`)·DC RAG TO(`dcrag-to`)는 존재한다. 실무협의(`pwrk-cnsltn-`\*) 엔티티·화면은 **미구현**. 버튼 매트릭스·상태코드는 **v2.1 잔존**이며 v3.0(`FNRSSTTS`)과 불일치한다. [9절](#9-현재-코드-vs-to-be-갭) 참고.
|
||||
|
||||
---
|
||||
|
||||
## 1. 문서 목적
|
||||
|
||||
재원협의 4대 주체(기초·광역·환경청·공단)의 **상태 전이·버튼·API**를 `apps/as`에 구현할 때 쓰는 프론트엔드 가이드이다.
|
||||
|
||||
v1.0은 공단 접수자의 **자료검증 → 초안생성 → 웹기안기**만 다뤘다. v2.0은 2026-09-03 완결 프로세스 기준으로 **신청부터 최종종결(**`TA`**)까지**, 실무협의·AI 3파이프라인·결재선·보완 루프를 포함한다.
|
||||
|
||||
상세 표·시퀀스·DTO는 본문에 넣지 않고 레퍼런스로 분리한다. 구현 중에는 본문의 규칙만 보고, 표가 필요하면 해당 파일을 연다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 문서 맵
|
||||
|
||||
| 파일 | 내용 | 언제 보는가 |
|
||||
| -------------------------------------------------------- | ------------------------------------------------------------------------- | ----------------------- |
|
||||
| [development-guide.md](./development-guide.md) (본 문서) | 도메인, 핵심 규칙, 화면 흐름, 갭, 구현 순서 | 일상 구현 |
|
||||
| [ref-process-status.md](./ref-process-status.md) | 4-Lane 시퀀스, Activity, `FNRSSTTS`, 보완 3루프, AI 타임아웃, 결재선 복사 | 상태 전이·폴링 UX |
|
||||
| [ref-screen-buttons.md](./ref-screen-buttons.md) | 38행 화면·역할·버튼 매트릭스, 버튼 가시성 | `button-config.ts` 개편 |
|
||||
| [ref-endpoint.md](./ref-endpoint.md) | 버튼→API 총괄, 단계별 입출력, FE 호출 금지 API, Gap Analysis | 엔티티·service 매핑 |
|
||||
| [ref-frontend-entities.md](./ref-frontend-entities.md) | ENDPOINT, 슬라이스, Service 시그니처, 폴링, 웹기안기, DTO | 엔티티 레이어 구현 |
|
||||
| [ref-webhwp-api.md](./ref-webhwp-api.md) | 웹한글기안기 v3 `HwpCtrl` Properties/Methods, Open/SaveAs | 기안기 위젯 연동 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 도메인 개요
|
||||
|
||||
실무협의서(PWRK)는 재원협의서(FNRS)의 **하위 문서**이다. 재원협의 1건당 실무협의서는 **1건만** 생성된다.
|
||||
|
||||
| 문서 | 번호 예시 | 필드 |
|
||||
| -------- | ----------------- | ----------------------------- |
|
||||
| 재원협의 | `F-20260824-0001` | `fnrsCnsltnDocNo` / `upDocNo` |
|
||||
| 실무협의 | `P-20260827-0001` | `pwrkCnsltnDocNo` |
|
||||
|
||||
진행상태는 **재원협의 마스터** `tb_as_fnrs_cnsltn_mng.prgrs_stts_com_cd` (`FNRSSTTS`)가 SSOT이다. 실무협의 문서의 `prgrsSttsComCd`는 연계 표시용이며, 화면 버튼은 재원협의 상태로 분기한다.
|
||||
|
||||
### 3-1. 4대 주체
|
||||
|
||||
| 역할 | 코드 | 화면 소유 | 핵심 구간 |
|
||||
| --------------- | ------------- | -------------- | ------------------------------------ |
|
||||
| 기초자치단체 | `ROLE_USER05` | 작성/수정/상세 | 신청, 3종 보완, 조치결과, 최종확인 |
|
||||
| 광역자치단체 | `ROLE_USER04` | 상세 | 경유 제출, 결과 전달 |
|
||||
| 유역·지방환경청 | `ROLE_USER03` | 상세 | 의뢰/자체종결, 1·2차 회신 |
|
||||
| 한국환경공단 | `ROLE_USER01` | 상세 + 결재선 | 파일검증, 검토자지정, 1·2차 기술검토 |
|
||||
|
||||
### 3-2. 핵심 식별자
|
||||
|
||||
| 필드 | 의미 | 비고 |
|
||||
| ----------------- | ---------------- | ----------------------------------------------------------- |
|
||||
| `fnrsCnsltnDocNo` | 재원협의문서번호 | 상태 전이·목록 PK |
|
||||
| `pwrkCnsltnDocNo` | 실무협의문서번호 | `P-YYYYMMDD-NNNN`. 검토자지정(`ER→AC`) 시 발급 |
|
||||
| `upDocNo` | 상위문서번호 | 재원협의 번호. `upDocSeComCd = "FNRS"` |
|
||||
| `crtDmndUuid` | 생성요청 UUID | 초안 폴링·웹기안기·다운로드 키 |
|
||||
| `verSn` | 버전일련번호 | 복합 PK `(crtDmndUuid, verSn)`. 저장 시 서버가 `+1` |
|
||||
| `orgnlOtptDocCn` | HWP 바이너리 | 목록/상세 응답은 항상 `null`. 다운로드·`webhwp/open`만 사용 |
|
||||
| `elaprCycl` | 결재 차수 | 1차 `TE`, 2차 `RK`. 금액검증 완료 후 1차 결재선 복사 |
|
||||
|
||||
### 3-3. IIPBS 약어
|
||||
|
||||
| 한글 | 약어 | 필드 예 |
|
||||
| ---- | ------------ | --------------------------- |
|
||||
| 재원 | FNRS | `fnrsCnsltnDocNo` |
|
||||
| 실무 | PWRK | `pwrkCnsltnDocNo` |
|
||||
| 협의 | CNSLTN | `fnrs-cnsltn-mng` |
|
||||
| 생성 | CRT | `crtDmndUuid` |
|
||||
| 요청 | DMND | `crtDmndUuid`, `elapr-dmnd` |
|
||||
| 검증 | VRFC | `vrfcSttsComCd` |
|
||||
| 보완 | SPLMNT | `splmntDmndList` |
|
||||
| 결재 | ELAPR / ATRZ | `elaprCycl`, `atrzSeComCd` |
|
||||
|
||||
---
|
||||
|
||||
## 4. 상태코드 한눈에 보기
|
||||
|
||||
상위코드 `FNRSSTTS`. v1.0의 `PWRKSTTS`(`DV`, `AR`, `TR` 등)는 **폐기**한다.
|
||||
|
||||
정방향:
|
||||
|
||||
```
|
||||
SV → ST → LE → FV → ER → AC → TE → TP → RM → RB → RC → RA → AV → EV → RK → RR → EP → TA
|
||||
```
|
||||
|
||||
자체종결: `LE → EP → TA`
|
||||
보완: `ER|TE|RK → SL` 후 직전 상태로 복귀.
|
||||
|
||||
| 코드 | 명칭 | FE에서 할 일 |
|
||||
| ---- | ---------------------- | ------------------------------------------------- |
|
||||
| `SV` | 저장 | 기초 작성화면 저장/신청/삭제 |
|
||||
| `ST` | 신청완료 | 광역 환경청제출 |
|
||||
| `LE` | 환경청 검토 | 자체종결(`EP`) 또는 기술검토 의뢰(`FV`) |
|
||||
| `FV` | 파일검증 | AI 진행 표시. **30분** 후 `ER` Fallback |
|
||||
| `ER` | 기술검토 의뢰 | 공단 접수자: 파일보완 / 수동파일검증 / 검토자지정 |
|
||||
| `SL` | 보완요청 | 기초 수정화면. 복귀 대상은 이력의 직전 상태 |
|
||||
| `AC` | 초안생성 | AI 진행 표시. **30분** 후 `TE` Fallback |
|
||||
| `TE` | 기술검토 | 웹기안기·검토완료·보완. 결재선 1차 |
|
||||
| `TP` | 기술검토 회신 | 환경청 → 광역 `RM` |
|
||||
| `RM` | 재원협의결과회신 | 광역 → 기초 `RB` |
|
||||
| `RB` | 지자체회신 | 기초 실무협의서 관리 / 조치완료 |
|
||||
| `RC` | 조치결과서 제출 | 광역 → 환경청 `RA` |
|
||||
| `RA` | 조치결과서제출(환경청) | 환경청 2차 의뢰 `AV` |
|
||||
| `AV` | 금액검증 | AI 진행 표시. **30분** 후 `EV` Fallback |
|
||||
| `EV` | 조치결과검토의뢰 | 공단 접수자: 2차 결재선 검토자지정 → `RK` |
|
||||
| `RK` | 조치결과서검토 | 웹기안기·금액검증 재실행·보완. 결재선 2차 |
|
||||
| `RR` | 조치결과서검토 회신 | 환경청 → 광역 `EP` |
|
||||
| `EP` | 검토결과 회신 | 광역 → 기초 `TA` |
|
||||
| `TA` | 검토결과 회신(종료) | 기초 조회만. **고시(**`TB`**) 아님** |
|
||||
|
||||
코드 의미·구버전 대조·보완 루프·AI Fallback은 [ref-process-status.md](./ref-process-status.md).
|
||||
|
||||
---
|
||||
|
||||
## 5. 프론트가 지켜야 할 핵심 규칙
|
||||
|
||||
1. **상태 SSOT는** `FNRSSTTS`. 버튼 가시성은 `(dtlInstCd, prgrsSttsComCd, atrzSeComCd)`로만 계산한다. 공단은 결재선 구분(`ATRZ01` 접수자, `ATRZ02` 검토자, `ATRZ03` 승인자, `ATRZ99` 최종승인자)을 추가로 본다.
|
||||
2. **AI 콜백 API는 화면에서 호출하지 않는다.** `POST /dcrag/documents/verify-result`, `POST /dcrag/draft/complete`, `POST /dcrag/cost/verify-result`는 서버 전용이다.
|
||||
3. **초안생성 1차 트리거는 검토자지정이다.** `POST /elapr-dmnd` (`ER → AC`). 예전처럼 `POST /pwrk-cnsltn-mng`로 초안을 시작하지 않는다.
|
||||
4. **기술검토 의뢰는** `LE → FV` **전용 API** `POST /fnrs-cnsltn-mng/{docNo}/request-examine`. 현재 코드의 `updatePrgrsStts(ER)` 직행은 폐기.
|
||||
5. **수동 AI 재실행은 상태를 바꾸지 않는다.**
|
||||
|
||||
- `ER` 파일검증 → `POST /dcrag-to/documents/transfer`
|
||||
- `TE` 초안생성 → `POST /dcrag-to/documents/draft/generate` (타임아웃·실패 시에만)
|
||||
- `RK` 금액검증 → `POST /dcrag-to/documents/cost/verify`
|
||||
|
||||
6. **진행상태만 바꾸는 버튼은** `POST /fnrs-cnsltn-mng/doc/updatePrgrsStts`**를 재사용한다.** (`ST→LE`, `RM→RB`, `RC→RA`, `RR→EP`, `EP→TA`)
|
||||
7. **보완요청/보완완료는 공통 API** `reqsupplement` / `submitsupplement`. 복귀 상태(`ER`/`TE`/`RK`)는 서버가 이력에서 결정한다. 프론트는 직전 상태를 추측해 보내지 않는다.
|
||||
8. **결재 승인/회신은** `PUT /elapr-prgrs-stts/{id}/process` **하나**. 1차 최종승인자 → `TP`, 2차 → `RR`은 서버가 Feign으로 처리한다.
|
||||
9. **2차 결재선은 EV에서 수동 검토자지정**. `POST /elapr-dmnd` (`elaprCycl: 2`, `docPrgrsSttsComCd: RK`). 1차 결재선 자동 copy-line은 사용하지 않는다.
|
||||
10. **웹기안기는 `webhwp/open` 응답을 axios `blob`으로 조회한 뒤 `File`로 감싸 `HwpCtrl.Open`에 전달한다.** `blob:` Object URL과 `orgnlOtptDocCn`은 쓰지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 화면 데이터 흐름
|
||||
|
||||
재원협의 **목록 → 상세/작성**이 본체다. 실무협의 목록은 공단 검토 보조 화면이다.
|
||||
|
||||
```
|
||||
BEGIN
|
||||
CALL fnrsCnsltnMngService.findPage(...)
|
||||
ON 행 CLICK → /fnrs-cnsltn/{fnrsCnsltnDocNo}
|
||||
|
||||
CALL findById(fnrsCnsltnDocNo)
|
||||
COMPUTE buttons = getButtonConfigs(dtlInstCd, prgrsSttsComCd, atrzSeComCdList)
|
||||
|
||||
// ----- 기초 ROLE_USER05 -----
|
||||
ON [저장] (빈값/SV) → saveApply(prgrsSttsComCd: SV)
|
||||
ON [신청완료] (SV) → saveApply(prgrsSttsComCd: ST)
|
||||
ON [삭제] (SV) → remove
|
||||
ON [파일보완완료] (SL←ER) → submitsupplement
|
||||
ON [보완완료] (SL←TE|RK) → submitsupplement
|
||||
ON [실무협의서 관리] (RB) → GET webhwp/open (blob) → HwpCtrl.Open(File)
|
||||
ON [조치완료] (RB) → submit-actn-result { isSubmit: true }
|
||||
ON [결과회신] (TA) → API 없음 (조회)
|
||||
|
||||
// ----- 광역 ROLE_USER04 -----
|
||||
ON [환경청제출] (ST) → updatePrgrsStts(LE)
|
||||
ON [지자체회신] (RM) → updatePrgrsStts(RB)
|
||||
ON [조치결과서제출] (RC) → updatePrgrsStts(RA)
|
||||
ON [결과회신] (EP) → updatePrgrsStts(TA)
|
||||
|
||||
// ----- 환경청 ROLE_USER03 -----
|
||||
ON [결과회신] (LE 자체종결) → tech-review → EP
|
||||
ON [기술검토 의뢰] (LE) → request-examine → FV → START POLL FV
|
||||
ON [재원협의 결과 회신] (TP) → tech-review → RM
|
||||
ON [실무협의서 관리] (RA) → GET webhwp/open (blob) → HwpCtrl.Open(File) (저장 불가)
|
||||
ON [조치결과 검토의뢰] (RA) → request-actn-review → AV → START POLL AV
|
||||
ON [결과회신] (RR) → updatePrgrsStts(EP)
|
||||
|
||||
// ----- 공단 ROLE_USER01 -----
|
||||
ON [파일보완] (ER, ATRZ01) → GET file-vrfc-results → 팝업 → reqsupplement
|
||||
ON [파일검증] (ER, ATRZ01) → dcrag-to transfer (상태 유지)
|
||||
ON [검토자지정] (ER, ATRZ01) → POST elapr-dmnd (docPrgrsSttsComCd: AC) → START POLL AC
|
||||
ON [검토자지정] (EV, ATRZ01) → POST elapr-dmnd (elaprCycl: 2, docPrgrsSttsComCd: RK)
|
||||
ON [실무협의서관리] (TE|RK) → GET webhwp/open (blob) → HwpCtrl.Open(File)
|
||||
ON [실무협의서생성] (TE, 실패/타임아웃) → dcrag-to draft/generate
|
||||
ON [검토결과조회] (TE) → pwrk-cnsltn-mng findById | findByUpDocNo
|
||||
ON [저장] (TE|RK) → pwrk-cnsltn-mng save-apply
|
||||
ON [검토완료] → elapr-prgrs-stts process (상신)
|
||||
ON [보완요청] (TE|RK) → 보완 팝업 → reqsupplement
|
||||
ON [금액검증] (RK) → dcrag-to cost/verify
|
||||
ON [결재(승인)] → process (중간)
|
||||
ON [결재(회신)] → process (최종 → TP 또는 RR)
|
||||
|
||||
// ----- AI 진행 중 (FV / AC / AV) -----
|
||||
POLL 상태 또는 draft status. 30분 안내. 종료 후 상세 invalidate
|
||||
END
|
||||
```
|
||||
|
||||
38행 버튼 표는 [ref-screen-buttons.md](./ref-screen-buttons.md). API 상세는 [ref-endpoint.md](./ref-endpoint.md).
|
||||
|
||||
---
|
||||
|
||||
## 7. 버튼 → API 축약
|
||||
|
||||
프론트가 치는 것만. 콜백·스케줄러는 제외.
|
||||
|
||||
| 주체 | 버튼 | From → To | Method & URI |
|
||||
| --------- | -------------------------- | ----------------------------- | ---------------------------------------------------------------------------------- |
|
||||
| 기초 | 저장 / 신청완료 | →`SV` / `SV→ST` | `POST /fnrs-cnsltn-mng/save-apply` |
|
||||
| 기초 | 삭제 | `SV` | `DELETE /fnrs-cnsltn-mng/{docNo}` |
|
||||
| 광역 | 환경청제출 | `ST→LE` | `POST /fnrs-cnsltn-mng/doc/updatePrgrsStts` |
|
||||
| 환경청 | 결과회신(자체종결) | `LE→EP` | `POST /fnrs-cnsltn-mng/{docNo}/tech-review` |
|
||||
| 환경청 | 기술검토 의뢰 | `LE→FV` | **신규** `POST /fnrs-cnsltn-mng/{docNo}/request-examine` |
|
||||
| 공단 접수 | 파일보완 팝업 | `ER` | `GET /fnrs-cnsltn-mng/{docNo}/file-vrfc-results` |
|
||||
| 공단 접수 | 파일보완요청 | `ER→SL` | `POST /fnrs-cnsltn-mng/{docNo}/reqsupplement` |
|
||||
| 공단 접수 | 파일검증 재실행 | `ER` 유지 | `POST /dcrag-to/documents/transfer` |
|
||||
| 공단 접수 | 검토자지정 | `ER→AC` | `POST /elapr-dmnd` (`elaprCycl: 1`) |
|
||||
| 공단 접수 | 검토자지정 | `EV→RK` | `POST /elapr-dmnd` (`elaprCycl: 2`) |
|
||||
| 공단 검토 | 실무협의서관리 | `TE`/`RK` | `GET .../pwrk-cnsltn-doc-crt-rslt/{uuid}/webhwp/open` |
|
||||
| 공단 검토 | 실무협의서생성 | `TE` 유지 | `POST /dcrag-to/documents/draft/generate` |
|
||||
| 공단 검토 | 저장 | `TE`/`RK` | `POST /pwrk-cnsltn-mng/save-apply` |
|
||||
| 공단 | 검토완료 / 결재(승인·회신) | `TE` 유지 / `TE→TP` / `RK→RR` | `PUT /elapr-prgrs-stts/{id}/process` |
|
||||
| 공단 검토 | 보완요청 | `TE→SL` / `RK→SL` | `POST /fnrs-cnsltn-mng/{docNo}/reqsupplement` |
|
||||
| 기초 | 파일보완완료 / 보완완료 | `SL→ER` / `TE` / `RK` | `POST /fnrs-cnsltn-mng/{docNo}/submitsupplement` |
|
||||
| 환경청 | 재원협의 결과 회신 | `TP→RM` | `POST /fnrs-cnsltn-mng/{docNo}/tech-review` |
|
||||
| 광역 | 지자체회신 | `RM→RB` | `updatePrgrsStts` |
|
||||
| 기초 | 실무협의서 관리 | `RB` 유지 | `GET .../pwrk-cnsltn-doc-crt-rslt/{uuid}/webhwp/open` |
|
||||
| 기초 | 조치완료 | `RB→RA` | **신규** `POST /pwrk-cnsltn-mng/{pwrkDocNo}/submit-actn-result` (`isSubmit: true`) |
|
||||
| 광역 | 조치결과서제출 | `RC→RA` | `updatePrgrsStts` |
|
||||
| 환경청 | 실무협의서 관리 | `RA` 유지 (조회) | `GET .../pwrk-cnsltn-doc-crt-rslt/{uuid}/webhwp/open` |
|
||||
| 환경청 | 조치결과 검토의뢰 | `RA→AV` | **신규** `POST /fnrs-cnsltn-mng/{docNo}/request-actn-review` |
|
||||
| 공단 검토 | 금액검증 재실행 | `RK` 유지 | `POST /dcrag-to/documents/cost/verify` |
|
||||
| 환경청 | 결과회신 | `RR→EP` | `updatePrgrsStts` |
|
||||
| 광역 | 결과회신 | `EP→TA` | `updatePrgrsStts` |
|
||||
| 웹기안기 | 저장 | 버전 +1 | `POST /pwrk-cnsltn-doc-crt-rslt` multipart |
|
||||
| 공통 | 다운로드 | — | `GET /pwrk-cnsltn-doc-crt-rslt/{uuid}/download` |
|
||||
|
||||
신규 4개: `request-examine`, `submit-actn-result`, `request-actn-review`, EV 2차 `elapr-dmnd`.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,334 @@
|
||||
# 레퍼런스 — 상태별 Endpoint
|
||||
|
||||
> 상위: [개발가이드.md](./개발가이드.md)
|
||||
> SoT: `05.재원협의_상태별_endpoint_설계.md` (v1.0, 2026-09-03)
|
||||
> Gateway: `/as-service`, `/as-elapr-service`
|
||||
> Auth: `Authorization`, `X-Site-Id`, `token-id` (기존 `client` 인터셉터)
|
||||
> 날짜: `yyyy-MM-dd HH:mm:ss`
|
||||
|
||||
프론트가 치는 API만 상세히 적는다. AI 콜백·스케줄러·Feign은 [§6 호출 금지](#6-프론트-호출-금지).
|
||||
|
||||
---
|
||||
|
||||
## 1. 총괄 매트릭스 (38 + WebHWP)
|
||||
|
||||
| 순번 | 버튼 | From → To | 서비스 | Method & URI | 구분 |
|
||||
| :---: | --- | --- | --- | --- | --- |
|
||||
| 1 | 저장 | →`SV` | as | `POST /api/v1/fnrs-cnsltn-mng/save-apply` | 기존 |
|
||||
| 2 | 신청완료 | `SV→ST` | as | 동일 (`prgrsSttsComCd=ST`) | 기존 |
|
||||
| 3 | 삭제 | `SV` | as | `DELETE /api/v1/fnrs-cnsltn-mng/{docNo}` | 기존 |
|
||||
| 4 | 환경청제출 | `ST→LE` | as | `POST /api/v1/fnrs-cnsltn-mng/doc/updatePrgrsStts` | 기존 |
|
||||
| 5 | 결과회신(자체종결) | `LE→EP` | as | `POST /api/v1/fnrs-cnsltn-mng/{docNo}/tech-review` | 기존 |
|
||||
| 6 | 기술검토 의뢰 | `LE→FV` | as | `POST /api/v1/fnrs-cnsltn-mng/{docNo}/request-examine` | **신규** |
|
||||
| 7 | 파일검증 콜백 | `FV→ER` | as | `POST /api/v1/dcrag/documents/verify-result` | 서버 |
|
||||
| 8 | 파일보완 팝업 | `ER` | as | `GET /api/v1/fnrs-cnsltn-mng/{docNo}/file-vrfc-results` | 기존 |
|
||||
| 9 | 파일보완요청 | `ER→SL` | as | `POST /api/v1/fnrs-cnsltn-mng/{docNo}/reqsupplement` | 기존 |
|
||||
| 10 | 파일검증 재실행 | `ER` | as | `POST /api/v1/dcrag-to/documents/transfer` | 기존 |
|
||||
| 11 | 검토자지정 | `ER→AC` | elapr | `POST /api/v1/elapr-dmnd` | 기존 |
|
||||
| 12 | 초안 콜백 | `AC→TE` | as | `POST /api/v1/dcrag/draft/complete` | 서버 |
|
||||
| 13 | 실무협의서관리 | `TE` | as | `GET /api/v1/pwrk-cnsltn-mng/updocno/{upDocNo}` | 기존 |
|
||||
| 14 | 검토결과조회 | `TE` | as | `GET /api/v1/pwrk-cnsltn-mng/{pwrkDocNo}` | 기존 |
|
||||
| 15 | 실무협의서생성 | `TE` | as | `POST /api/v1/dcrag-to/documents/draft/generate` | 기존 |
|
||||
| 16 | 저장(실무) | `TE` | as | `POST /api/v1/pwrk-cnsltn-mng/save-apply` | 기존 |
|
||||
| 17 | 검토완료 | `TE` | elapr | `PUT /api/v1/elapr-prgrs-stts/{id}/process` | 기존 |
|
||||
| 18 | 보완요청 | `TE→SL` | as | `POST .../reqsupplement` | 기존 |
|
||||
| 19 | 결재(승인) | `TE` | elapr | `PUT .../process` | 기존 |
|
||||
| 20 | 결재(회신) | `TE→TP` | elapr | `PUT .../process` | 기존 |
|
||||
| 21 | 파일보완완료 | `SL→ER` | as | `POST .../submitsupplement` | 기존 |
|
||||
| 22 | 보완완료(기술) | `SL→TE` | as | 동일 | 기존 |
|
||||
| 23 | 보완완료(조치) | `SL→RK` | as | 동일 | 기존 |
|
||||
| 24 | 재원협의 결과 회신 | `TP→RM` | as | `POST .../{docNo}/tech-review` | 기존 |
|
||||
| 25 | 지자체회신 | `RM→RB` | as | `POST .../doc/updatePrgrsStts` | 기존 |
|
||||
| 26 | 실무협의서 관리 | `RB` | as | `GET .../pwrk-cnsltn-doc-crt-rslt/{uuid}/webhwp/open` | 기존 |
|
||||
| 27 | 조치완료 | `RB→RA` | as | `POST /api/v1/pwrk-cnsltn-mng/{pwrkDocNo}/submit-actn-result` (`isSubmit=true`) | **신규** |
|
||||
| 28 | 조치결과서제출 | `RC→RA` | as | `updatePrgrsStts` | 기존 |
|
||||
| 29 | 조치결과 검토의뢰 | `RA→AV` | as | `POST /api/v1/fnrs-cnsltn-mng/{docNo}/request-actn-review` | **신규** |
|
||||
| 30 | 금액검증 콜백 | `AV→EV` | as | `POST /api/v1/dcrag/cost/verify-result` | 서버 |
|
||||
| 30a | 검토자지정(2차) | `EV→RK` | elapr | `POST /api/v1/elapr-dmnd` (`elaprCycl=2`) | 기존 |
|
||||
| 31 | 실무협의서관리 | `RK` | as | `GET /pwrk-cnsltn-mng/{pwrkDocNo}` | 기존 |
|
||||
| 32 | 금액검증 재실행 | `RK` | as | `POST /api/v1/dcrag-to/documents/cost/verify` | 기존 |
|
||||
| 33 | 보완요청(금액) | `RK→SL` | as | `reqsupplement` | 기존 |
|
||||
| 34 | 결재(승인) | `RK` | elapr | `process` | 기존 |
|
||||
| 35 | 결재(회신) | `RK→RR` | elapr | `process` | 기존 |
|
||||
| 36 | 결과회신 | `RR→EP` | as | `updatePrgrsStts` | 기존 |
|
||||
| 37 | 결과회신 | `EP→TA` | as | `updatePrgrsStts` | 기존 |
|
||||
| 38 | 결과회신(확인) | `TA` | — | 없음 | — |
|
||||
| W1 | 웹기안기 Open | — | as | `GET /api/v1/pwrk-cnsltn-doc-crt-rslt/{uuid}/webhwp/open` | 기존 |
|
||||
| W2 | HWPX 저장 | — | as | `POST /api/v1/pwrk-cnsltn-doc-crt-rslt` multipart | 기존 |
|
||||
| W3 | 다운로드 | — | as | `GET /api/v1/pwrk-cnsltn-doc-crt-rslt/{uuid}/download` | 기존 |
|
||||
| E-N | 결재선 복사 | `AV→RK` 부수 | elapr | `POST /api/v1/elapr-apln/copy-line` | **신규·서버** |
|
||||
|
||||
조회 공통: `GET /fnrs-cnsltn-mng`, `GET /fnrs-cnsltn-mng/{docNo}`, `GET /pwrk-cnsltn-mng`, `GET /dcrag-to/documents/draft/status/{crtDmndUuid}`.
|
||||
|
||||
---
|
||||
|
||||
## 2. 공통 요청
|
||||
|
||||
### `DocPrgrsUpdateRequestDto`
|
||||
|
||||
`POST /fnrs-cnsltn-mng/doc/updatePrgrsStts` — `ST→LE`, `RM→RB`, `RC→RA`, `RR→EP`, `EP→TA`.
|
||||
|
||||
```json
|
||||
{
|
||||
"docNo": "F-20260824-0001",
|
||||
"docSeComCd": "FNRS",
|
||||
"prgrsSttsComCd": "LE",
|
||||
"rgtrUuid": "optional-uuid",
|
||||
"rmrk": "optional"
|
||||
}
|
||||
```
|
||||
|
||||
응답: `204/200` void. 프론트 엔티티: 기존 `changePrgrsSttsComCd`.
|
||||
|
||||
### `FnrsCnsltnApplySaveRequestDto`
|
||||
|
||||
`save-apply`, `tech-review`, `reqsupplement`, `submitsupplement`가 공유. 핵심은 `fnrsCnsltnMng.prgrsSttsComCd`와 중첩 리스트(`bzScaleList`, `bizCstList`, `cklstResultList`, `esntlAtchFileList`, `splmntDmndList`).
|
||||
|
||||
---
|
||||
|
||||
## 3. 단계별 FE API
|
||||
|
||||
### 3-1. `SV → ST → LE`
|
||||
|
||||
**저장 / 신청완료** — `POST /fnrs-cnsltn-mng/save-apply`
|
||||
권한: `ROLE_USER05`. 임시저장은 `prgrsSttsComCd=SV`, 신청은 `ST`.
|
||||
응답: `string` (문서번호). 기존 `fnrsCnsltnMngService.saveApply`.
|
||||
|
||||
**삭제** — `DELETE /fnrs-cnsltn-mng/{docNo}` (`SV`만).
|
||||
|
||||
**환경청제출** — `updatePrgrsStts` `LE`. 권한: `ROLE_USER04`.
|
||||
|
||||
### 3-2. `LE → FV | EP`
|
||||
|
||||
**자체종결** — `POST /fnrs-cnsltn-mng/{docNo}/tech-review`
|
||||
Body: `FnrsCnsltnApplySaveRequestDto` (환경청 의견·체크리스트). `LE→EP`.
|
||||
기존 `techReview`. 현재 화면은 `TP`에서도 이 API를 쓰며 목표 상태를 `EP`로 넣고 있다 → **`TP`는 `RM`으로 바꿔야 함**.
|
||||
|
||||
**기술검토 의뢰 (신규)** — `POST /fnrs-cnsltn-mng/{docNo}/request-examine`
|
||||
|
||||
```json
|
||||
{ "prgrsSttsComCd": "FV" }
|
||||
```
|
||||
|
||||
응답: `string` (docNo). `LE→FV` + AI 파일검증 + 30분 타이머(서버).
|
||||
프론트: `fnrsCnsltnMngService.requestExamine(docNo, body)` 추가. **`updatePrgrsStts("ER")` 금지.**
|
||||
|
||||
### 3-3. `FV → ER → SL → ER`
|
||||
|
||||
파일검증 콜백은 서버. FE는 `FV`에서 상세 refetch.
|
||||
|
||||
**검증결과 목록** — `GET /fnrs-cnsltn-mng/{fnrsCnsltnDocNo}/file-vrfc-results`
|
||||
응답: `FnrsCnsltnFileVrfcRsltResponseDto[]`
|
||||
|
||||
| 필드 | 설명 |
|
||||
| --- | --- |
|
||||
| `fileSeComCd` | `ESSENTIAL` / `ADDITIONAL` |
|
||||
| `fileSeNm` | 구분명 |
|
||||
| `esntlAtchFileId` | 필수첨부 ID |
|
||||
| `atchFileNm` | 파일명 |
|
||||
| `vrfcRsltComCd` | `PASS` / `FAIL` / `WARN` |
|
||||
| `vrfcOpnn` | 검증의견 |
|
||||
|
||||
엔티티에 `findFileVrfcResults` 추가. 첨부 행 배지는 상세 `esntlFileList`/`etcFileList`의 `vrfcSttsComCd`, `vrfcRsltCn`, `vrfcDt`도 사용 (타입 확장 필요).
|
||||
|
||||
**파일보완요청** — `POST .../{docNo}/reqsupplement` (`ER→SL`).
|
||||
**파일보완완료** — `POST .../{docNo}/submitsupplement` (`SL→ER`). 복귀는 서버.
|
||||
|
||||
**파일검증 재실행** — `POST /dcrag-to/documents/transfer`
|
||||
|
||||
```json
|
||||
{ "fnrsCnsltnDocNo": "F-20260824-0001", "taskSeComCd": "SF", "taskSeCdNm": "하수처리시설" }
|
||||
```
|
||||
|
||||
응답: `{ resultCode, message, requestId }`. 상태 유지. 기존 `dcRagApiToService.requestDocumentTransfer`.
|
||||
|
||||
### 3-4. `ER → AC → TE → TP`
|
||||
|
||||
**검토자지정** — `POST /elapr-dmnd`
|
||||
기존 `elaprDmndService.save`.
|
||||
|
||||
| 필드 | 값 |
|
||||
| --- | --- |
|
||||
| `elaprDocNo` | 재원협의 번호 |
|
||||
| `elaprCycl` | `1` |
|
||||
| `docPrgrsSttsComCd` | **`AC`** (현행은 현재 상태 `ER`를 넣고 있음 → 수정) |
|
||||
| `elaprPrgrsSttsList[].atrzOrdr` | 1 검토자, 2 승인자, 3 최종 |
|
||||
|
||||
서버가 실무협의 문서 생성 + RAG 초안 트리거 + 30분 타이머. 응답의 문서번호/`crtDmndUuid`가 있으면 폴링 키로 쓴다. 없으면 `findByUpDocNo`로 복원.
|
||||
|
||||
**초안 상태 폴링** — `GET /dcrag-to/documents/draft/status/{crtDmndUuid}`
|
||||
기존 `getDraftGenerationStatus`. 주기 5~10초. 종료 `COMPLETED`/`FAILED`.
|
||||
|
||||
**수동 초안생성** — `POST /dcrag-to/documents/draft/generate` (`TE` 유지)
|
||||
|
||||
```json
|
||||
{
|
||||
"fnrsCnsltnDocNo": "F-...",
|
||||
"pwrkCnsltnDocNo": "P-...",
|
||||
"crtDmndUuid": "optional"
|
||||
}
|
||||
```
|
||||
|
||||
202 + `DcRagDraftGenerateResponseDto` (`crtDmndUuid`, `crtSttsComCd`, `estimatedMinutes`).
|
||||
v1.0 가이드의 “이 API를 프론트가 치면 안 된다”는 **폐기**. 1차 트리거는 검토자지정이고, 이 API는 Fallback 전용.
|
||||
|
||||
**실무 저장** — `POST /pwrk-cnsltn-mng/save-apply` → `string`.
|
||||
**보완요청/완료** — fnrs `reqsupplement` / `submitsupplement`.
|
||||
|
||||
**결재** — `PUT /elapr-prgrs-stts/{elaprPrgrsId}/process`
|
||||
|
||||
```json
|
||||
{
|
||||
"atrzPrgrsSttsComCd": "ATRZ_PRGRS_003",
|
||||
"atrzOpnnCn": "의견",
|
||||
"finalApprovalYn": false
|
||||
}
|
||||
```
|
||||
|
||||
반려: `ATRZ_PRGRS_004`. 기존 `elaprPrgrsSttsService.process`. 최종 시 서버가 `TP`/`RR` 전이.
|
||||
|
||||
### 3-5. `TP → RM → RB → RA`
|
||||
|
||||
**환경청 1차 회신** — `tech-review` (`TP→RM`).
|
||||
**광역 지자체회신** — `updatePrgrsStts` `RB`.
|
||||
|
||||
**조치완료 (신규)** — `POST /pwrk-cnsltn-mng/{pwrkDocNo}/submit-actn-result`
|
||||
|
||||
```json
|
||||
{
|
||||
"isSubmit": true,
|
||||
"actnList": [
|
||||
{
|
||||
"rvwOpnnSn": 1,
|
||||
"actnRsltCn": "수용 — 관로 연장 재산정",
|
||||
"atchFileGroupId": "optional"
|
||||
}
|
||||
],
|
||||
"totActnSummary": "제출 시 권장",
|
||||
"muniDocNo": "제출 시 권장"
|
||||
}
|
||||
```
|
||||
|
||||
- `isSubmit: false` → `RB` 유지 (API만 존재, **화면 버튼 없음**. HWP·첨부는 실무협의서 관리 다이얼로그에서 저장)
|
||||
- `isSubmit: true` → `RB→RA` (조치완료)
|
||||
응답: `string` (pwrkDocNo). DTO명 `PwrkActnResultSubmitDto`.
|
||||
|
||||
### 3-6. `RA → AV → EV → RK`
|
||||
**2차 의뢰 (신규)** — `POST /fnrs-cnsltn-mng/{docNo}/request-actn-review`
|
||||
|
||||
```json
|
||||
{
|
||||
"specialRqstCn": "중점 검증 요청 (선택)"
|
||||
}
|
||||
```
|
||||
|
||||
`RA→AV`. 응답: `string` (docNo). DTO명 `EnvoActnReviewReqDto`. `specialRqstCn`만 선택.
|
||||
|
||||
금액검증 콜백은 서버 → `AV→EV`.
|
||||
**2차 검토자지정** — `POST /elapr-dmnd` (`elaprCycl: 2`, `docPrgrsSttsComCd: RK`). 1차 결재선(`findDocPrgrsByDocNo`, cycl=1)을 시드로 편집.
|
||||
|
||||
**금액검증 재실행** — `POST /dcrag-to/documents/cost/verify`
|
||||
|
||||
```json
|
||||
{
|
||||
"fnrsCnsltnDocNo": "F-...",
|
||||
"pwrkCnsltnDocNo": "P-..."
|
||||
}
|
||||
```
|
||||
|
||||
202. 기존 `requestCostVerification`. `RK` 유지.
|
||||
|
||||
### 3-7. `RK → RR | SL`
|
||||
|
||||
보완은 3-4와 동일 API. 팝업만 금액 체크.
|
||||
결재 `process` 2차 최종 → 서버가 `RR`.
|
||||
|
||||
### 3-8. `RR → EP → TA`
|
||||
|
||||
둘 다 `updatePrgrsStts`. `TA` 확인은 API 없음.
|
||||
|
||||
---
|
||||
|
||||
## 4. 웹기안기
|
||||
|
||||
설계서 기준 **기존**. 구현 시 컨트롤러 존재 여부를 확인한다.
|
||||
|
||||
| ID | 용도 | URI | 응답 |
|
||||
| --- | --- | --- | --- |
|
||||
| W1 | 웹기안기 Open | `GET .../pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/webhwp/open` | `inline` HWPX blob → `HwpCtrl.Open(File)` |
|
||||
| W2 | 편집 저장 | `POST .../pwrk-cnsltn-doc-crt-rslt` multipart `file` + `crtDmndUuid` | 201, `verSn+1` |
|
||||
| W3 | PC 저장 | `GET .../{crtDmndUuid}/download` | `attachment` blob |
|
||||
|
||||
버전 지정: `GET .../{crtDmndUuid}/{verSn}/webhwp/open`, `.../{verSn}/download`.
|
||||
`/{uuid}/latest`, `/{uuid}/download`가 `/{uuid}/{verSn}`보다 우선. path를 섞지 말 것.
|
||||
|
||||
다운로드:
|
||||
|
||||
- axios `responseType: "blob"`
|
||||
- 파일명: `Content-Disposition` `filename*=UTF-8''...` (`parseFilenameFromResponse`)
|
||||
- 서버 파일명: `재원협의 실무협의서({bizNm})_v{verSn}.hwp`
|
||||
- Content-Type은 `application/x-hwp` 또는 `application/haansofthwpx`일 수 있음. 타입에 의존하지 말 것
|
||||
|
||||
Open은 axios `responseType: "blob"`으로 인증된 API를 호출하고, 응답 Blob을 HWPX `File`로 감싸 `HwpCtrl.Open`에 전달한다. `blob:` Object URL은 사용하지 않는다.
|
||||
|
||||
multipart `Content-Type` 헤더를 수동 지정하지 않는다 (boundary). JSON `saveJson`은 UI에서 쓰지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 전자결재 (`as-elapr-service`)
|
||||
|
||||
| API | URI | FE |
|
||||
| --- | --- | --- |
|
||||
| 검토자지정·1차 결재요청 | `POST /elapr-dmnd` | O |
|
||||
| 결재 처리 | `PUT /elapr-prgrs-stts/{id}/process` | O |
|
||||
| 결재선 복사 | `POST /elapr-apln/copy-line` | **X (서버)** |
|
||||
|
||||
`copy-line` body (참고만):
|
||||
|
||||
```json
|
||||
{
|
||||
"docNo": "P-20260827-0001",
|
||||
"sourceElaprCycl": 1,
|
||||
"targetElaprCycl": 2,
|
||||
"rgtrUuid": "system-uuid"
|
||||
}
|
||||
```
|
||||
|
||||
`RK`에서 결재현황은 `findDocPrgrsByDocNo`(전체) 조회 후 `elaprCycl`로 1차/2차 탭 표시.
|
||||
|
||||
---
|
||||
|
||||
## 6. 프론트 호출 금지
|
||||
|
||||
| URI | 이유 |
|
||||
| --- | --- |
|
||||
| `POST /dcrag/documents/verify-result` | AI→AS 콜백 |
|
||||
| `POST /dcrag/draft/complete` | AI→AS 콜백 |
|
||||
| `POST /dcrag/cost/verify-result` | AI→AS 콜백. 여기서 copy-line도 호출 |
|
||||
| `POST /elapr-apln/copy-line` | 콜백 Feign |
|
||||
| `POST /pwrk-cnsltn-mng` (`save` 초안 트리거) | v3.0 1차 트리거는 `elapr-dmnd`. 컨트롤러가 남아 있어도 화면에서 쓰지 않음 |
|
||||
|
||||
`POST /dcrag-to/documents/draft/generate`는 **Fallback에서만** 허용.
|
||||
|
||||
---
|
||||
|
||||
## 7. Gap Analysis (프론트 엔티티)
|
||||
|
||||
### 이미 있는 것
|
||||
|
||||
`fnrsCnsltnMngService`: `findPage`, `findById`, `save`, `saveApply`, `update`, `techReview`, `submitSupplement`, `submitReqSupplement`, `remove`, `changePrgrsSttsComCd`
|
||||
`dcRagApiToService`: `requestDocumentTransfer`, `requestDraftGeneration`, `getDraftGenerationStatus`, `requestCostVerification`
|
||||
`elaprDmndService.save`, `elaprPrgrsSttsService.process`
|
||||
|
||||
### 추가할 것
|
||||
|
||||
| 메서드 | URI |
|
||||
| --- | --- |
|
||||
| `requestExamine` | `POST /fnrs-cnsltn-mng/{id}/request-examine` |
|
||||
| `findFileVrfcResults` | `GET /fnrs-cnsltn-mng/{id}/file-vrfc-results` |
|
||||
| `requestActnReview` | `POST /fnrs-cnsltn-mng/{id}/request-actn-review` |
|
||||
| `pwrkCnsltnMngService.*` | 슬라이스 신규 |
|
||||
| `submitActnResult` | `POST /pwrk-cnsltn-mng/{id}/submit-actn-result` |
|
||||
| `pwrkCnsltnDocCrtRsltService.*` | 슬라이스 신규 |
|
||||
|
||||
`copyLine`은 프론트 서비스에 넣지 않는다.
|
||||
@@ -0,0 +1,350 @@
|
||||
# 레퍼런스 — 프론트 엔티티·DTO
|
||||
|
||||
> 상위: [개발가이드.md](./개발가이드.md)
|
||||
> 엔티티 규칙: `frontend/.cursor/skills/backend-controller-to-entity`
|
||||
|
||||
---
|
||||
|
||||
## 1. ENDPOINT
|
||||
|
||||
`apps/as/src/shared/config/api.ts`
|
||||
|
||||
```ts
|
||||
AS_SERVICE: {
|
||||
// 기존
|
||||
FNRS_CNSLTN_MNG: "/as-service/api/v1/fnrs-cnsltn-mng",
|
||||
DCRAG_TO: "/as-service/api/v1/dcrag-to",
|
||||
// 추가
|
||||
PWRK_CNSLTN_MNG: "/as-service/api/v1/pwrk-cnsltn-mng",
|
||||
PWRK_CNSLTN_DOC_CRT_RSLT: "/as-service/api/v1/pwrk-cnsltn-doc-crt-rslt",
|
||||
}
|
||||
|
||||
AS_ELAPR_SERVICE: {
|
||||
ELAPR_TMPLT: "/as-elapr-service/api/v1/elapr-tmplt",
|
||||
ELAPR_APLN: "/as-elapr-service/api/v1/elapr-apln",
|
||||
ELAPR_DMND: "/as-elapr-service/api/v1/elapr-dmnd",
|
||||
ELAPR_PRGRS_STTS: "/as-elapr-service/api/v1/elapr-prgrs-stts",
|
||||
}
|
||||
```
|
||||
|
||||
`copy-line`은 `ELAPR_APLN` 하위이나 FE에서 호출하지 않음.
|
||||
|
||||
---
|
||||
|
||||
## 2. 슬라이스 배치
|
||||
|
||||
```
|
||||
entities/fnrs-cnsltn/ # 확장
|
||||
entities/dcrag-to/ # 구현됨
|
||||
entities/elapr/ # 구현됨
|
||||
entities/pwrk-cnsltn-mng/ # 신규
|
||||
entities/pwrk-cnsltn-doc-crt-rslt/ # 신규
|
||||
```
|
||||
|
||||
```
|
||||
entities/pwrk-cnsltn-mng/
|
||||
├── model/
|
||||
│ ├── pwrk-cnsltn-mng-interface.ts
|
||||
│ └── pwrk-cnsltn-mng-payload-schema.ts
|
||||
├── api/
|
||||
│ ├── pwrk-cnsltn-mng-service.ts
|
||||
│ └── queries.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
`submit-actn-result` Request는 zod SSOT (`PwrkActnResultSubmitDto`).
|
||||
`request-examine` / `request-actn-review`도 fnrs payload-schema에 추가.
|
||||
|
||||
라우트:
|
||||
|
||||
| 화면 | 경로 |
|
||||
| --- | --- |
|
||||
| 재원협의 목록/상세/작성 | 기존 `app/(main)/(with-base-layout)/fnrs-cnsltn/...` |
|
||||
| 실무협의 목록 | `.../pwrk-cnsltn/page.tsx` (선택) |
|
||||
| 실무협의 상세 | `.../pwrk-cnsltn/[id]/page.tsx` (선택) |
|
||||
| 페이지 UI | `pages/fnrs-cnsltn/`, `pages/pwrk-cnsltn/` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Service 시그니처
|
||||
|
||||
`fnrsCnsltnMngService`에 추가:
|
||||
|
||||
```ts
|
||||
requestExamine(docNo: string, body: FnrsExamineRequestDto): Promise<string>;
|
||||
findFileVrfcResults(fnrsCnsltnDocNo: string): Promise<FnrsCnsltnFileVrfcRsltResponseDto[]>;
|
||||
requestActnReview(docNo: string, body: EnvoActnReviewReqDto): Promise<string>;
|
||||
```
|
||||
|
||||
실무협의:
|
||||
|
||||
```ts
|
||||
export interface PwrkCnsltnMngService {
|
||||
findPage(searchDto: PwrkCnsltnMngSearchDto): Promise<PageResponse<PwrkCnsltnMngResponseDto>>;
|
||||
findById(pwrkCnsltnDocNo: string): Promise<PwrkCnsltnMngDetResponseDto>;
|
||||
findByUpDocNo(upDocNo: string): Promise<PwrkCnsltnMngDetResponseDto>;
|
||||
saveApply(body: PwrkCnsltnApplySaveRequestDto): Promise<string>;
|
||||
submitActnResult(
|
||||
pwrkCnsltnDocNo: string,
|
||||
body: PwrkActnResultSubmitDto,
|
||||
): Promise<string>;
|
||||
update(pwrkCnsltnDocNo: string, body: PwrkCnsltnMngUpdateRequestDto): Promise<string>;
|
||||
remove(pwrkCnsltnDocNo: string): Promise<void>;
|
||||
changePrgrsSttsComCd(body: DocPrgrsUpdateRequestDto): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
v1.0의 `save(): PwrkCnsltnMngSaveResponseDto`(초안 트리거)는 화면에서 사용하지 않는다. 컨트롤러가 남아 있으면 엔티티에 넣더라도 UI 연결 금지.
|
||||
|
||||
생성결과:
|
||||
|
||||
```ts
|
||||
export interface PwrkCnsltnDocCrtRsltService {
|
||||
findPage(
|
||||
searchDto: PwrkCnsltnDocCrtRsltSearchDto,
|
||||
): Promise<PageResponse<PwrkCnsltnDocCrtRsltResponseDto>>;
|
||||
findById(crtDmndUuid: string, verSn: number): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
|
||||
findLatestByCrtDmndUuid(crtDmndUuid: string): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
|
||||
openWebHwpLatest(crtDmndUuid: string): Promise<AxiosResponse<Blob>>;
|
||||
download(crtDmndUuid: string, verSn: number): Promise<AxiosResponse<Blob>>;
|
||||
downloadLatest(crtDmndUuid: string): Promise<AxiosResponse<Blob>>;
|
||||
saveMultipart(formData: FormData): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
|
||||
updateMultipart(
|
||||
crtDmndUuid: string,
|
||||
verSn: number,
|
||||
formData: FormData,
|
||||
): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
|
||||
remove(crtDmndUuid: string, verSn: number): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
`webhwp/open`은 인증된 axios 요청으로 Blob을 조회한다:
|
||||
|
||||
```ts
|
||||
async function openWebHwpLatest(crtDmndUuid: string) {
|
||||
return client.get<Blob>(`${BASE}/${crtDmndUuid}/webhwp/open`, {
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
응답 Blob은 HWPX `File`로 감싸 `HwpCtrl.Open(file, "HWPX", ...)`에 전달한다. `blob:` Object URL은 사용하지 않는다.
|
||||
|
||||
DC RAG TO (구현됨, 시그니처 유지):
|
||||
|
||||
```ts
|
||||
requestDocumentTransfer(body: DcRagDocumentTransferRequestDto): Promise<DcRagCommonResponseDto>;
|
||||
requestDraftGeneration(body: DcRagDraftGenerateRequestDto): Promise<DcRagDraftGenerateResponseDto>;
|
||||
getDraftGenerationStatus(crtDmndUuid: string): Promise<DcRagDraftStatusResponseDto>;
|
||||
requestCostVerification(body: DcRagCostVerifyRequestDto): Promise<DcRagCommonResponseDto>;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. DTO
|
||||
|
||||
타입명 = 백엔드 클래스명. Save/Apply는 zod.
|
||||
|
||||
### 4-1. 재원협의 추가
|
||||
|
||||
**`FnrsExamineRequestDto`**
|
||||
|
||||
| 필드 | 필수 | 값 |
|
||||
| --- | --- | --- |
|
||||
| `prgrsSttsComCd` | N | 기본 `FV` |
|
||||
|
||||
**`EnvoActnReviewReqDto`**
|
||||
|
||||
| 필드 | 필수 |
|
||||
| --- | --- |
|
||||
| `specialRqstCn` | N |
|
||||
|
||||
**`FnrsCnsltnFileVrfcRsltResponseDto`**: [ref-endpoint.md](./ref-endpoint.md) §3-3.
|
||||
|
||||
첨부 검증 배지용으로 `AtflMngClientResponseDto` / `EsntlAtchFileMngTreeResponseDto`에 `vrfcDt`, `vrfcRsltCn`, `vrfcSttsComCd` 확장.
|
||||
|
||||
### 4-2. `PwrkActnResultSubmitDto` (zod)
|
||||
|
||||
| 필드 | 필수 | 비고 |
|
||||
| --- | --- | --- |
|
||||
| `isSubmit` | Y | `false` API만(화면 버튼 없음), `true` 조치완료(`RB→RA`) |
|
||||
| `actnList` | N | 조치완료 시 선택 |
|
||||
| `actnList[].rvwOpnnSn` | Y (항목 있을 때) | |
|
||||
| `actnList[].actnRsltCn` | N | |
|
||||
| `actnList[].atchFileGroupId` | N | |
|
||||
| `totActnSummary` | N | 제출 시 권장 |
|
||||
| `muniDocNo` | N | 제출 시 권장 |
|
||||
| `pwrkAtchFileGroupId` | N | |
|
||||
|
||||
### 4-3. `PwrkCnsltnMng*`
|
||||
|
||||
**Search**: `pwrkCnsltnDocNo`, `upDocNo`, `upDocSeComCd`, `prgrsSttsComCd`, `admdstCd`, `envoSeComCd`, `taskSeComCd`, `bizNm`
|
||||
|
||||
**DetResponse 중첩**
|
||||
|
||||
- `cnsltnList`: 협의내역 (`rvwOpnnSn`, `rvwOpnnClsfComCd`, `cnsltnCn`, `rfltYn`, …)
|
||||
- `pjtcoAjmtList`: 사업비조정
|
||||
- `crtDmndList[].crtRslt`: 생성요청 + 최신 결과
|
||||
- `splmntDmndList`: 보완요청
|
||||
- `sttsChgHstryList`: 상태변경이력 — `SL` 복귀 라벨에 사용
|
||||
- `trsmFileList`: 전송파일 (Java Det DTO 확인 후 반영)
|
||||
|
||||
마스터 조인: `bizNm`, `admdstCd`, `envoSeComCd`, `taskSeComCd`, `aplcntNm` 등.
|
||||
|
||||
`crtRslt.orgnlOtptDocCn`은 항상 `null`.
|
||||
|
||||
**상세 예**
|
||||
|
||||
```json
|
||||
{
|
||||
"pwrkCnsltnDocNo": "P-20260827-0001",
|
||||
"upDocNo": "F-20260824-0001",
|
||||
"upDocSeComCd": "FNRS",
|
||||
"prgrsSttsComCd": "TE",
|
||||
"cnsltnList": [],
|
||||
"pjtcoAjmtList": [],
|
||||
"crtDmndList": [
|
||||
{
|
||||
"crtDmndUuid": "d0545bef-735f-4d7b-a781-88e96e03577a",
|
||||
"pwrkCnsltnDocNo": "P-20260827-0001",
|
||||
"rqstrUuid": "...",
|
||||
"rqstrNm": "관리자",
|
||||
"crtSttsComCd": "C",
|
||||
"crtRslt": {
|
||||
"crtDmndUuid": "d0545bef-735f-4d7b-a781-88e96e03577a",
|
||||
"verSn": 1,
|
||||
"rgtrNm": "관리자",
|
||||
"orgnlOtptDocCn": null,
|
||||
"crtCmptnDt": "2026-08-27 10:47:01"
|
||||
}
|
||||
}
|
||||
],
|
||||
"splmntDmndList": [],
|
||||
"sttsChgHstryList": []
|
||||
}
|
||||
```
|
||||
|
||||
### 4-4. `PwrkCnsltnDocCrtRslt*`
|
||||
|
||||
복합 PK: `crtDmndUuid` + `verSn`
|
||||
|
||||
**Response**
|
||||
|
||||
| 필드 | 타입 | 비고 |
|
||||
| --- | --- | --- |
|
||||
| `crtDmndUuid` | `string` | |
|
||||
| `verSn` | `number` | |
|
||||
| `rgtrUuid` / `rgtrNm` | `string` | |
|
||||
| `orgnlOtptDocCn` | `null` | 목록/상세 미포함 |
|
||||
| `crtCmptnDt` | `string` | |
|
||||
|
||||
**Search** (`PageableParams`): `crtDmndUuid`(이력 조회 시 필수), `pwrkCnsltnDocNo`, `verSn`, `rgtrUuid`
|
||||
|
||||
```http
|
||||
GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt?page=0&size=10&crtDmndUuid={uuid}&sort=verSn,desc
|
||||
```
|
||||
|
||||
**Save (zod)**: `crtDmndUuid` 필수. `rgtrUuid` 생략 시 서버 로그인 사용자. `file`은 multipart part (DTO 필드 아님).
|
||||
|
||||
### 4-5. DC RAG (구현됨)
|
||||
|
||||
```ts
|
||||
interface DcRagDocumentTransferRequestDto {
|
||||
fnrsCnsltnDocNo: string;
|
||||
taskSeComCd?: string;
|
||||
taskSeCdNm?: string;
|
||||
}
|
||||
|
||||
interface DcRagDraftStatusResponseDto {
|
||||
crtDmndUuid: string;
|
||||
status: DraftStatus;
|
||||
statusName?: string;
|
||||
progressPercent?: number;
|
||||
events?: EventInfoDto[];
|
||||
startedDt?: string;
|
||||
estimatedCompletionDt?: string;
|
||||
errorMessage?: string | null;
|
||||
}
|
||||
|
||||
type DraftStatus =
|
||||
| "REQUESTED"
|
||||
| "DOWNLOADING"
|
||||
| "ANALYZING"
|
||||
| "GENERATING"
|
||||
| "COMPLETED"
|
||||
| "FAILED";
|
||||
```
|
||||
|
||||
`estimatedMinutes`는 초안 응답에 있을 수 있으나 UX 안내는 **30분 Fallback**을 따른다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 폴링 훅
|
||||
|
||||
초안 (`AC` 또는 `TE`이면서 생성 중):
|
||||
|
||||
```ts
|
||||
useQuery({
|
||||
queryKey: [...dcragToKeys.draftStatus(crtDmndUuid)],
|
||||
queryFn: () => dcRagApiToService.getDraftGenerationStatus(crtDmndUuid),
|
||||
enabled: Boolean(crtDmndUuid) && isPolling,
|
||||
refetchInterval: (query) => {
|
||||
const status = query.state.data?.status;
|
||||
if (status === "COMPLETED" || status === "FAILED") return false;
|
||||
return 5_000;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`COMPLETED` 시 `fnrs-cnsltn` 상세·`pwrk-cnsltn-mng` `findByUpDocNo` invalidate.
|
||||
|
||||
`FV`/`AV`는 `findById`를 5~10초 refetch. `prgrsSttsComCd`가 `ER`/`RK`가 되면 중단. 시작 시각 기준 30분 경과 안내 문구.
|
||||
|
||||
`crtDmndUuid` 복원: `findByUpDocNo(fnrsCnsltnDocNo).crtDmndList[0]`.
|
||||
|
||||
---
|
||||
|
||||
## 6. 다운로드
|
||||
|
||||
```ts
|
||||
const response = await client.get<Blob>(url, { responseType: "blob" });
|
||||
const filename = parseFilenameFromResponse(response);
|
||||
const objectUrl = window.URL.createObjectURL(response.data);
|
||||
```
|
||||
|
||||
기존 `attachService`와 동일.
|
||||
|
||||
---
|
||||
|
||||
## 7. 실무협의 목록 컬럼 (선택 화면)
|
||||
|
||||
| 표시명 | Field |
|
||||
| --- | --- |
|
||||
| 실무협의번호 | `pwrkCnsltnDocNo` |
|
||||
| 재원협의번호 | `upDocNo` |
|
||||
| 진행상태 | `prgrsSttsComCdNm` |
|
||||
| 사업명 | `bizNm` |
|
||||
| 시도/구군 | `sido` / `gugun` |
|
||||
| 신청자 | `aplcntNm` |
|
||||
| 등록일 | `regDt` |
|
||||
|
||||
행 클릭 → `/pwrk-cnsltn/{pwrkCnsltnDocNo}` 또는 재원협의 상세로 이동 (화면설계 확정 후).
|
||||
|
||||
---
|
||||
|
||||
## 8. `elapr-dmnd` 저장 시 주의
|
||||
|
||||
현행 `floating-action-buttons.tsx`:
|
||||
|
||||
```ts
|
||||
elaprCycl: data.detDto?.prgrsSttsComCd === "RK" ? 2 : 1,
|
||||
docPrgrsSttsComCd: data.detDto?.prgrsSttsComCd, // ER
|
||||
```
|
||||
|
||||
To-Be:
|
||||
|
||||
- 검토자지정은 **`ER`(1차)** 과 **`EV`(2차)** 에서.
|
||||
- `ER`: `elaprCycl: 1`, `docPrgrsSttsComCd: "AC"`, 서식 선택 다이얼로그
|
||||
- `EV`: `elaprCycl: 2`, `docPrgrsSttsComCd: "RK"`, 1차 결재선 시드 다이얼로그(서식 없음)
|
||||
|
||||
`RK` / ATRZ02 FAB: `findDocPrgrsByDocNo` 전체 조회 후 pending은 `elaprCycl === 2`만 사용. 검토완료 후 `ElaprApprovalDialog`에 `elaprCycl={2}`. 보완요청은 TE와 동일 `SplmntDmndDialog`.
|
||||
@@ -0,0 +1,246 @@
|
||||
# 레퍼런스 — 프로세스·상태 전이
|
||||
|
||||
> 상위: [개발가이드.md](./개발가이드.md)
|
||||
> SoT: `03.재원협의_실무협의_프로세스_통합_v완결.md` (v3.0, 2026-09-03)
|
||||
|
||||
---
|
||||
|
||||
## 1. 프로세스 개요
|
||||
|
||||
| 항목 | 내용 |
|
||||
| --- | --- |
|
||||
| Process No. | PR-AS-003 |
|
||||
| 메뉴 | 행정지원 > 2.2.2 재원협의 > 2.2.2.1 재원협의 관리 |
|
||||
| 목적 | 기초 → 광역(경유) → 환경청(승인) → 공단(기술검토/AI) 4자 협의로 사업규모·국비·분담금을 확정 |
|
||||
|
||||
2-Pass:
|
||||
|
||||
- **Pass 1** 기술 타당성: `SV→ST→LE→FV→ER→AC→TE→TP→RM→RB`
|
||||
- **Pass 2** 조치결과·금액: `RB→RA→AV→EV→RK→RR→EP→TA`
|
||||
|
||||
자체종결(기술검토 생략): `LE→EP→TA`
|
||||
|
||||
---
|
||||
|
||||
## 2. 4-Lane 시퀀스
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor 기초 as 기초 (ROLE_USER05)
|
||||
actor 광역 as 광역 (ROLE_USER04)
|
||||
actor 환경청 as 환경청 (ROLE_USER03)
|
||||
actor 공단 as 공단 (ROLE_USER01)
|
||||
actor AI as AI 엔진
|
||||
|
||||
rect rgb(240, 248, 255)
|
||||
기초->>기초: [저장] SV
|
||||
기초->>광역: [신청완료] ST
|
||||
광역->>환경청: [환경청제출] LE
|
||||
end
|
||||
|
||||
rect rgb(255, 250, 240)
|
||||
alt 자체종결
|
||||
환경청->>광역: [결과회신] LE → EP
|
||||
광역->>기초: [결과회신] EP → TA
|
||||
else 기술검토 의뢰
|
||||
환경청->>AI: [기술검토 의뢰] LE → FV
|
||||
end
|
||||
end
|
||||
|
||||
rect rgb(245, 255, 245)
|
||||
AI-->>공단: 파일검증 콜백 FV → ER
|
||||
Note over 공단: 30분 미응답 시 ER + 수동 [파일검증]
|
||||
alt 파일 미비
|
||||
공단->>기초: [파일보완] ER → SL
|
||||
기초->>공단: [파일보완완료] SL → ER
|
||||
else 적격
|
||||
공단->>AI: [검토자지정] ER → AC
|
||||
end
|
||||
end
|
||||
|
||||
rect rgb(255, 245, 245)
|
||||
AI-->>공단: 초안 콜백 AC → TE
|
||||
Note over 공단: 30분 미응답 시 TE + 수동 [실무협의서생성]
|
||||
alt 설계 미비
|
||||
공단->>기초: [보완요청] TE → SL
|
||||
기초->>공단: [보완완료] SL → TE
|
||||
else 검토 완료
|
||||
공단->>공단: [검토완료] 상신, [결재(승인)] TE 유지
|
||||
공단->>환경청: [결재(회신)] TE → TP
|
||||
end
|
||||
end
|
||||
|
||||
rect rgb(250, 240, 255)
|
||||
환경청->>광역: [재원협의 결과 회신] TP → RM
|
||||
광역->>기초: [지자체회신] RM → RB
|
||||
기초->>환경청: [조치완료] RB → RA
|
||||
end
|
||||
|
||||
rect rgb(240, 255, 255)
|
||||
광역->>환경청: [조치결과서제출] RC → RA
|
||||
환경청->>AI: [조치결과 검토의뢰] RA → AV
|
||||
AI-->>공단: 금액검증 콜백 AV → EV
|
||||
공단->>공단: [검토자지정] EV → RK
|
||||
Note over 공단: EV에서 1차 결재선 시드로 2차 결재선 수동 배정
|
||||
end
|
||||
|
||||
rect rgb(255, 255, 230)
|
||||
alt 소명 미비
|
||||
공단->>기초: [보완요청] RK → SL
|
||||
기초->>공단: [보완완료] SL → RK
|
||||
else 확정
|
||||
공단->>환경청: [결재(회신)] RK → RR
|
||||
end
|
||||
end
|
||||
|
||||
rect rgb(255, 240, 240)
|
||||
환경청->>광역: [결과회신] RR → EP
|
||||
광역->>기초: [결과회신] EP → TA
|
||||
end
|
||||
```
|
||||
|
||||
거버넌스:
|
||||
|
||||
```
|
||||
기초 (USER05) --ST--> 광역 (USER04) --LE--> 환경청 (USER03)
|
||||
기초 <-----TA------ 광역 <-----EP/RM------ 환경청
|
||||
환경청 --FV/ER--> 공단 (USER01) --TP/RR--> 환경청
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Activity 정의 (14)
|
||||
|
||||
| No. | Activity | 주체 | From → To | FE 포인트 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | 재원협의 조회 | 기초 | — | 기본계획·자가점검 대조 |
|
||||
| 2 | 등록 및 제출 | 기초·광역 | `SV→ST`, 광역 `LE` | `save-apply`, `updatePrgrsStts` |
|
||||
| 3 | 접수 및 의뢰 | 환경청 | `LE→FV` / `EP` | 의뢰 vs 자체종결 분기 |
|
||||
| 4 | AI 파일검증 | 공단·AI | `FV→ER` / `SL` | 30분 Fallback, 파일보완 팝업 |
|
||||
| 5 | 자체종결 회신 | 환경청·광역 | `LE→EP→TA` | 공단 레인 없음 |
|
||||
| 6 | 검토자지정·초안 | 공단·AI | `ER→AC→TE` | `elapr-dmnd`, 초안 폴링 |
|
||||
| 7 | 1차 기술검토 | 공단 결재선 | `TE` / `SL` | 웹기안기, HWP 파싱은 서버 |
|
||||
| 8 | 기술검토 회신 | 공단 최종 | `TE→TP` | `process` 최종 |
|
||||
| 9 | 검토결과 회신 | 환경청·광역 | `TP→RM→RB` | 1차 회신. `EP`와 혼동 금지 |
|
||||
| 10 | 조치결과 작성 | 기초 | `RB→RA` | `submit-actn-result` `isSubmit:true` (조치완료) |
|
||||
| 11 | 조치 의뢰 | 환경청 | `RA→AV` | `request-actn-review` |
|
||||
| 12 | AI 금액검증 | AI·공단 | `AV→EV` | 콜백/Fallback 후 접수자 대기 |
|
||||
| 12a | 2차 검토자지정 | 공단 접수 | `EV→RK` | `elapr-dmnd` cycl=2 |
|
||||
| 13 | 2차 검토·결재 | 공단 | `RK→RR` / `SL` | 2차 결재선 |
|
||||
| 14 | 최종 회신 | 환경청·광역·기초 | `RR→EP→TA` | 종결 |
|
||||
|
||||
---
|
||||
|
||||
## 4. `FNRSSTTS` 상세
|
||||
|
||||
관리: `tb_cs_lwpo_cd` / `tb_as_fnrs_cnsltn_mng.prgrs_stts_com_cd`
|
||||
|
||||
| 코드 | 국문 | 영문 | 화면 주체 | 전이 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `SV` | 저장 | Save | 기초 작성 | 신규/`SV` → `SV` |
|
||||
| `ST` | 신청완료 | Submit to Province | 기초→광역 | `SV→ST` |
|
||||
| `LE` | 환경청 검토 | Local Environment Review | 광역→환경청 | `ST→LE` |
|
||||
| `FV` | 파일검증 | File Verification | AI | `LE→FV` |
|
||||
| `ER` | 기술검토 의뢰 | Examine Request | 공단 접수자 | `FV→ER`, `SL→ER` |
|
||||
| `SL` | 보완요청 | Supplement Loop | 공단→기초 | `ER\|TE\|RK → SL` |
|
||||
| `AC` | 초안생성 | AI Auto Creation | AI | `ER→AC` |
|
||||
| `TE` | 기술검토 | Technical Examine | 공단 1차 결재 | `AC→TE`, `SL→TE` |
|
||||
| `TP` | 기술검토 회신 | Technical Pass to Basin | 공단→환경청 | `TE→TP` |
|
||||
| `RM` | 재원협의결과회신 | Result Message to Province | 환경청→광역 | `TP→RM` |
|
||||
| `RB` | 재원협의결과회신(지자체) | Result Back to Municipality | 광역→기초 | `RM→RB` |
|
||||
| `RC` | 조치결과서 제출 | Result Complete | 기초→광역 | `RB→RC` |
|
||||
| `RA` | 조치결과서 제출(환경청) | Result Ask to Basin | 기초→환경청 | `RB→RA` |
|
||||
| `AV` | 금액검증 | Amount Verification | AI | `RA→AV` |
|
||||
| `EV` | 조치결과검토의뢰 | Examine Verify | 공단 접수자 | `AV→EV` |
|
||||
| `RK` | 조치결과서검토 | Result Check | 공단 2차 결재 | `EV→RK`, `SL→RK` |
|
||||
| `RR` | 조치결과서검토 회신 | Result Return to Basin | 공단→환경청 | `RK→RR` |
|
||||
| `EP` | 검토결과 회신 | Examine Pass to Province | 환경청→광역 | `LE→EP`(자체), `RR→EP`(최종) |
|
||||
| `TA` | 검토결과 회신(종료) | Technical Approval | 광역→기초 | `EP→TA` |
|
||||
|
||||
### 4-1. v2.1 → v3.0 대조 (프론트 폐기 대상)
|
||||
|
||||
| 구간 | v2.1 | v3.0 |
|
||||
| --- | --- | --- |
|
||||
| 의뢰 직후 파일검증 | `ER` | **`FV` → `ER`** |
|
||||
| 검토자지정 후 초안 | `EE` → `DV` | **`ER` → `AC` → `TE`** |
|
||||
| 1차 내부 결재 | `TR` → `TP` | **`TE` 유지 → `TP`** |
|
||||
| 환경청→광역 1차 회신 | `EP` | **`RM`** |
|
||||
| 금액검증 | `RA` | **`RA` → `AV` → `RK`** |
|
||||
| 공단 2차 회신 | `TP` | **`RR`** |
|
||||
| 최종 완료 | `TP` / 고시 `TB` | **`TA`** (고시 없음) |
|
||||
|
||||
가이드 v1.0 `PWRKSTTS`(`DV`, `AR`, `TR`, `AA` 등)도 동일하게 폐기.
|
||||
|
||||
---
|
||||
|
||||
## 5. 보완(`SL`) 3루프
|
||||
|
||||
복귀 상태는 `tb_as_doc_stts_chg_hstry` 직전 코드. 프론트는 복귀 코드를 요청에 넣지 않고, 라벨만 이력으로 고른다.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
ER --> SL: 파일보완 팝업(파일리스트)
|
||||
SL --> ER: 파일보완완료
|
||||
TE --> SL: 보완 검토팝업()
|
||||
SL --> TE: 보완완료
|
||||
RK --> SL: 보완 검토팝업(금액)
|
||||
SL --> RK: 보완완료
|
||||
```
|
||||
|
||||
| 루프 | 팝업 | 요청 | 제출 버튼 |
|
||||
| --- | --- | --- | --- |
|
||||
| 파일 무결성 | 파일리스트 + 검증결과 | 공단 접수자 `ER` | [파일보완완료] |
|
||||
| 1차 기술 | 설계 미달·사유 | 공단 검토자 `TE` | [보완완료] |
|
||||
| 2차 금액 | 공종별 금액 체크 | 공단 검토자 `RK` | [보완완료] |
|
||||
|
||||
---
|
||||
|
||||
## 6. AI 3파이프라인·30분 Fallback
|
||||
|
||||
| ID | 단계 | 진입 → 완료 | 프론트 진입 | Fallback | 수동 재실행 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| AI-1 | 파일 무결성 | `FV→ER` | 환경청 [기술검토 의뢰] | 30분 → `ER` + [파일검증] | `POST /dcrag-to/documents/transfer` |
|
||||
| AI-2 | 초안 생성 | `AC→TE` | 공단 [검토자지정] | 30분 → `TE` + [실무협의서생성] | `POST /dcrag-to/documents/draft/generate` |
|
||||
| AI-3 | 금액 검증 | `AV→EV` | 환경청 [조치결과 검토의뢰] | 30분 → `EV` + 수동 [금액검증] | `POST /dcrag-to/documents/cost/verify` |
|
||||
|
||||
- 예상 소요 안내는 **30분** (v1.0의 10분은 폐기).
|
||||
- 콜백 수신·스케줄러 전이는 서버. FE는 상세 refetch / draft status 폴링만.
|
||||
- `AC→TE` 성공 시에만 [실무협의서관리]·[검토결과조회] 활성. 실패·타임아웃이면 [실무협의서생성].
|
||||
|
||||
초안 폴링 이벤트 예:
|
||||
|
||||
| `eventType` | `eventMessage` |
|
||||
| --- | --- |
|
||||
| `REQUESTED` | 요청을 접수했습니다. |
|
||||
| `DOWNLOADING` | 신청서를 수신하는 중입니다. |
|
||||
| `ANALYZING` | 자료 준비가 완료되었습니다. |
|
||||
| `GENERATING` | 문서 저장이 완료되었습니다. |
|
||||
| `COMPLETED` | 문서 생성이 완료되었습니다. |
|
||||
| `FAILED` | `errorMessage` 표시 |
|
||||
|
||||
종료: `status === "COMPLETED" | "FAILED"` 또는 `events`에 해당 `eventType`.
|
||||
|
||||
---
|
||||
|
||||
## 7. 2차 결재선 (수동 검토자지정)
|
||||
|
||||
1. `TE`에서 1차 결재선(`elaprCycl = 1`) 완료.
|
||||
2. 금액검증 콜백/Fallback → `AV→EV`.
|
||||
3. 공단 접수자가 `EV`에서 [검토자지정]: 1차 결재선을 시드로 편집 후 `POST /elapr-dmnd` (`elaprCycl: 2`) → `RK`.
|
||||
4. `copy-line` 자동 복사는 사용하지 않는다.
|
||||
|
||||
결재 `process`:
|
||||
|
||||
- 상신·중간 승인: 문서 상태 `TE` 또는 `RK` 유지.
|
||||
- 1차 최종: 서버가 `TE→TP`.
|
||||
- 2차 최종: 서버가 `RK→RR`.
|
||||
- 반려: 직전 결재자 복귀. 문서 상태 유지.
|
||||
|
||||
---
|
||||
|
||||
## 8. 웹기안기
|
||||
|
||||
- `TE`/`RK` [실무협의서관리] → `webhwp/open` Blob 조회 후 팝업에서 `HwpCtrl.Open(File)`.
|
||||
- [검토완료] 시 HWP 파싱·DB 등재는 **서버**. FE는 결재 `process`만.
|
||||
- 편집본 저장: `POST /pwrk-cnsltn-doc-crt-rslt` multipart. `verSn`은 클라이언트가 보내지 않음.
|
||||
@@ -0,0 +1,181 @@
|
||||
# 레퍼런스 — 화면·버튼 매트릭스
|
||||
|
||||
> 상위: [개발가이드.md](./개발가이드.md)
|
||||
> SoT: `03` §4 화면 및 버튼별 상세 상태전이 명세서 (Row 4~42, 38행)
|
||||
|
||||
구현 위치: `pages/fnrs-cnsltn/lib/button-config.ts` + `ui/components/floating-action-buttons.tsx`
|
||||
|
||||
---
|
||||
|
||||
## 1. 화면 구분
|
||||
|
||||
| 화면 | 소유 | 라우트 (현행) | 비고 |
|
||||
| --- | --- | --- | --- |
|
||||
| 작성화면 | 기초 `ROLE_USER05` | `/fnrs-cnsltn/new`, `/fnrs-cnsltn/{id}/edit` | `SV` |
|
||||
| 수정화면 | 기초 | `/fnrs-cnsltn/{id}/edit` | `SL` 보완 |
|
||||
| 상세화면 | 광역·환경청·공단·기초(`RB`/`TA`) | `/fnrs-cnsltn/{id}` | 조회 + 액션 버튼 |
|
||||
| 자동화 | AI | 버튼 없음 | `FV`/`AC`/`AV` 진행 패널 |
|
||||
|
||||
공단 상세는 결재선 구분으로 같은 상태라도 버튼이 다르다.
|
||||
|
||||
| `atrzSeComCd` | 역할 |
|
||||
| --- | --- |
|
||||
| `ATRZ01` | 접수자 |
|
||||
| `ATRZ02` | 검토자 |
|
||||
| `ATRZ03` | 승인자 |
|
||||
| `ATRZ99` | 최종승인자 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 38행 매트릭스
|
||||
|
||||
API 컬럼은 [ref-endpoint.md](./ref-endpoint.md)와 동일.
|
||||
|
||||
| 순번 | 화면 | 권한 | 결재선 | 상태 | 버튼 | 전이 | 다음 담당 | FE 동작 |
|
||||
| :---: | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 1 | 작성 | USER05 | - | 빈값, `SV` | 저장 | `SV` | 본인 | `save-apply` `SV` |
|
||||
| 2 | 작성 | USER05 | - | `SV` | 신청완료 | `ST` | 광역 | `save-apply` `ST` |
|
||||
| 3 | 작성 | USER05 | - | `SV` | 삭제 | 삭제 | — | `DELETE` |
|
||||
| 4 | 상세 | USER04 | - | `ST` | 환경청제출 | `LE` | 환경청 | `updatePrgrsStts` |
|
||||
| 5 | 상세 | USER03 | - | `LE` | 결과회신 | `EP` | 광역 | `tech-review` 자체종결 |
|
||||
| 6 | 상세 | USER03 | - | `LE` | 기술검토 의뢰 | `FV` | 공단 접수 | **`request-examine`** |
|
||||
| 7 | 자동화 | AI | - | `FV` | (파일검증) | `ER` | 공단 접수 | FE 호출 없음. 진행 패널 |
|
||||
| 8 | 상세 | USER01 | 접수자 | `ER` | 파일보완 | `SL` | 기초 | 팝업(파일리스트) → `reqsupplement` |
|
||||
| 9 | 상세 | USER01 | 접수자 | `ER` | 파일검증 | `ER` | 본인 | `dcrag-to/transfer` 재실행 |
|
||||
| 10 | 상세 | USER01 | 접수자 | `ER` | 검토자지정 | `AC` | 다음 결재자 | `POST /elapr-dmnd` |
|
||||
| 11 | 자동화 | AI | - | `AC` | (초안생성) | `TE` | 공단 검토 | FE 호출 없음. 폴링 |
|
||||
| 12 | 상세 | USER01 | 검토자 | `TE` | 실무협의서관리 | 유지 | — | 웹기안기 Open. **초안 성공 시만** |
|
||||
| 13 | 상세 | USER01 | 검토자 | `TE` | 검토결과조회 | 유지 | — | 실무협의 상세 |
|
||||
| 14 | 상세 | USER01 | 검토자 | `TE` | 실무협의서생성 | 유지 | — | **실패/타임아웃/초안 없음** 시 |
|
||||
| 15 | 상세 | USER01 | 검토자 | `TE` | 저장 | `TE` | — | `pwrk save-apply` |
|
||||
| 16 | 상세 | USER01 | 검토자 | `TE` | 검토완료 | `TE` | 다음 결재자 | `elapr process` 상신. 반려 시 직전 결재자 |
|
||||
| 17 | 상세 | USER01 | 검토자 | `TE` | 보완요청 | `SL` | 기초 | 보완 팝업() → `reqsupplement` |
|
||||
| 18 | 상세 | USER01 | 승인자 | `TE` | 결재(승인) | `TE` | 다음 결재자 | `process` |
|
||||
| 19 | 상세 | USER01 | 최종승인자 | `TE` | 결재(회신) | `TP` | 환경청 | `process` 최종 |
|
||||
| 20 | 수정 | USER05 | - | `SL` | 파일보완완료 | `ER` | 공단 접수 | `submitsupplement` (직전=`ER`) |
|
||||
| 21 | 수정 | USER05 | - | `SL` | 보완완료 | `TE` | 공단 검토 | `submitsupplement` (직전=`TE`) |
|
||||
| 22 | 수정 | USER05 | - | `SL` | 보완완료 | `RK` | 공단 검토 | `submitsupplement` (직전=`RK`) |
|
||||
| 23 | 상세 | USER03 | - | `TP` | 재원협의 결과 회신 | `RM` | 광역 | `tech-review` |
|
||||
| 24 | 상세 | USER04 | - | `RM` | 지자체회신 | `RB` | 기초 | `updatePrgrsStts` |
|
||||
| 25 | 상세 | USER05 | - | `RB` | 실무협의서 관리 | 유지 | — | 웹기안기 Open |
|
||||
| 26 | 상세 | USER05 | - | `RB` | 조치완료 | `RA` | 환경청 | **`submit-actn-result` `isSubmit:true`** |
|
||||
| 27 | 상세 | USER04 | - | `RC` | 조치결과서제출 | `RA` | 환경청 | `updatePrgrsStts` |
|
||||
| 28 | 상세 | USER03 | - | `RA` | 실무협의서 관리 | 유지 | — | 웹기안기 Open. **저장 불가** |
|
||||
| 28a | 상세 | USER03 | - | `RA` | 조치결과 검토의뢰 | `AV` | 공단 검토 | **`request-actn-review`** |
|
||||
| 29 | 자동화 | AI | - | `AV` | (금액검증) | `EV` | 공단 접수 | FE 호출 없음. 진행 패널 |
|
||||
| 29a | 상세 | USER01 | 접수자 | `EV` | 검토자지정 | `RK` | 공단 검토 | `POST /elapr-dmnd` cycl=2. 1차 결재선 시드 |
|
||||
| 30 | 상세 | USER01 | 검토자 | `RK` | 실무협의서관리 | 유지 | — | 웹기안기 |
|
||||
| 31 | 상세 | USER01 | 검토자 | `RK` | 금액검증 | `RK` | — | `dcrag-to/cost/verify` |
|
||||
| 32 | 상세 | USER01 | 검토자 | `RK` | 저장 | `RK` | — | `pwrk save-apply` |
|
||||
| 33 | 상세 | USER01 | 검토자 | `RK` | 검토완료 | `RK` | 다음 결재자 | `process` |
|
||||
| 34 | 상세 | USER01 | 승인자 | `RK` | 결재(승인) | `RK` | 다음 결재자 | `process` |
|
||||
| 35 | 상세 | USER01 | 최종승인자 | `RK` | 결재(회신) | `RR` | 환경청 | `process` 최종 |
|
||||
| 36 | 상세 | USER03 | - | `RR` | 실무협의서 관리(조회), 검토결과 회신 | `EP` | 광역 | `tech-review` |
|
||||
| 37 | 작성/상세 | USER04 | - | `EP` | 결과회신, 실무협의서 관리(조회) | `TA` | 기초 | `updatePrgrsStts` |
|
||||
| 38 | 상세 | USER05 | - | `TA` | 결과회신, 실무협의서 관리(조회) | — | — | **API 없음. 조회** |
|
||||
|
||||
`SL` 20~22행: 버튼은 직전 상태에 따라 **하나만** 노출. 파일 루프면 [파일보완완료], 그 외 [보완완료].
|
||||
|
||||
---
|
||||
|
||||
## 3. `button-config` 권장 구조
|
||||
|
||||
액션 키는 현행 `FNRS_CNSLTN_VIEW_BUTTON_ACTION`을 확장한다. 신규·이름 변경:
|
||||
|
||||
| 신규/변경 액션 | 라벨 | 비고 |
|
||||
| --- | --- | --- |
|
||||
| `fileSplmnt` | 파일보완 | `ER` 접수자. 현행에 없음 |
|
||||
| `fileVrfc` | 파일검증 | 유지. `ER` 재실행 |
|
||||
| `dsgntRvwr` | 검토자지정 | `ER→AC`(1차), `EV→RK`(2차) |
|
||||
| `pwrkCnsltnDocMng` | 실무협의서관리 | 유지. `TE`·`RK`·`RB`·`RA`(조회 전용) |
|
||||
| `pwrkCnsltnDocCrt` | 실무협의서생성 | 유지. 실패/타임아웃만 |
|
||||
| `rvwRsltInq` | 검토결과조회 | 신규 |
|
||||
| `actnCmptn` | 조치완료 | `RB→RA`. `isSubmit:true` |
|
||||
| `actnSbmsn` | 조치결과서제출 | 광역 `RC→RA` |
|
||||
| `rqstActnRvw` | 조치결과 검토의뢰 | 환경청 `RA→AV` |
|
||||
| `costVrfc` | 금액검증 | `RK` |
|
||||
| `lclRply` | 지자체회신 | 광역 `RM→RB` |
|
||||
| `fnrsRsltRply` | 재원협의 결과 회신 | 환경청 `TP→RM` |
|
||||
| `rsltRply` | 결과회신 | `LE→EP`, `RR→EP`, `EP→TA`, `TA` 조회 |
|
||||
| `fileSplmntCmptn` | 파일보완완료 | `SL←ER` |
|
||||
| `splmntCmptn` | 보완완료 | `SL←TE\|RK` |
|
||||
|
||||
**삭제**: `fnrsCnsltnAncmnt`(고시→`TB`), `sncnAprv`(인가승인). `EP`/`TA`는 `rsltRply`로 통일.
|
||||
|
||||
### 3-1. 역할 × 상태 (공단 제외)
|
||||
|
||||
```
|
||||
ROLE_USER05
|
||||
"" | SV → 저장, 신청완료, 삭제
|
||||
SL → (직전 ER) 파일보완완료 | (직전 TE|RK) 보완완료
|
||||
RB → 실무협의서 관리, 조치완료
|
||||
TA → 결과회신 (조회), 실무협의서 관리(조회)
|
||||
|
||||
ROLE_USER04
|
||||
ST → 환경청제출 // 현행 반려(rjct→SV)는 SoT 38행에 없음. 유지 시 별도 합의
|
||||
RM → 지자체회신
|
||||
RC → 조치결과서제출
|
||||
EP → 결과회신, 실무협의서 관리(조회)
|
||||
|
||||
ROLE_USER03
|
||||
LE → 결과회신(자체종결), 기술검토 의뢰
|
||||
TP → 재원협의 결과 회신
|
||||
RA → 실무협의서 관리(조회), 조치결과 검토의뢰
|
||||
RR → 실무협의서 관리(조회), 검토결과 회신 (`tech-review` → `EP`)
|
||||
```
|
||||
|
||||
### 3-2. 공단 결재선 × 상태
|
||||
|
||||
```
|
||||
ER / ATRZ01 접수자
|
||||
→ 파일보완, 파일검증, 검토자지정
|
||||
|
||||
EV / ATRZ01 접수자
|
||||
→ 검토자지정 (2차 결재선. 1차 결재선 시드, 서식 선택 없음)
|
||||
|
||||
TE / ATRZ02 검토자
|
||||
→ (초안 성공) 실무협의서관리, 검토결과조회, 저장, 검토완료, 보완요청
|
||||
→ (초안 실패·타임아웃) 실무협의서생성 (+ 위 항목 중 관리/조회는 초안 생긴 뒤)
|
||||
|
||||
TE / ATRZ03 승인자 → 결재(승인)
|
||||
TE / ATRZ99 최종 → 결재(회신)
|
||||
|
||||
RK / ATRZ02 검토자
|
||||
→ (초안 완료) 실무협의서관리
|
||||
→ (초안 완료 + 결재 대기 ATRZ_PRGRS_002, cycl2) 보완요청, 검토완료
|
||||
· 보완요청 UI: TE와 동일 SplmntDmndDialog
|
||||
RK / ATRZ03 → (초안 완료) 실무협의서관리, (cycl2 pending) 결재(승인)
|
||||
RK / ATRZ99 → (초안 완료) 실무협의서관리, (cycl2 pending) 결재(회신)
|
||||
· 결재 UI: ElaprApprovalDialog (elaprCycl=2) 승인/반려 → PUT process
|
||||
```
|
||||
|
||||
`FV`, `AC`, `AV`: 액션 빈 배열 + 진행 패널.
|
||||
|
||||
`PENDING_APPROVAL_ACTIONS`: `dmndSplmnt`, `rvwCmptn`, `atrzAprv`, `atrzRply`만.
|
||||
`pwrkCnsltnDocMng`는 결재 대기와 무관(초안 완료만). ATRZ02·ATRZ03·ATRZ99라도 pending 없으면 관리만 노출.
|
||||
|
||||
---
|
||||
|
||||
## 4. 초안 성공 vs 실패 버튼
|
||||
|
||||
`TE`에서 `crtDmndList[].crtSttsComCd` / 폴링 `status`로 분기.
|
||||
|
||||
| 조건 | 노출 |
|
||||
| --- | --- |
|
||||
| `C` / `COMPLETED` | 실무협의서관리, 검토결과조회, 저장, 검토완료, 보완요청 |
|
||||
| `F` / `FAILED` 또는 30분 Fallback 후 초안 없음 | 실무협의서생성 |
|
||||
| `R`/`I` 생성 중 | 진행 패널. 위 버튼 비활성 (`AC`와 동일 UX, 상태가 이미 `TE`면 패널+생성 버튼) |
|
||||
|
||||
`RK`는 금액검증 성공이 기본. Fallback 후 [금액검증] 재실행만 추가 노출.
|
||||
|
||||
---
|
||||
|
||||
## 5. 현행 `button-config.ts`와의 차이
|
||||
|
||||
| 항목 | 현재 | To-Be |
|
||||
| --- | --- | --- |
|
||||
| USER05 | `""/SV/TA(고시)` | `SL`, `RB` 추가. `TA`는 조회 |
|
||||
| USER04 | `ST`(제출+반려), `EP`(인가승인) | `RM/RC/EP` 추가. 인가승인 삭제 |
|
||||
| USER03 | `LE`, `TP`(→EP로 회신) | `RA`, `RR` 추가. `TP`는 `RM` |
|
||||
| USER01 | `ER`(파일검증, 검토자지정), `TE` | `ER`에 파일보완. `RK` 전면 추가 |
|
||||
| USER01 ATRZ | `ER`, `TE`만 `needsRoleUser01AtrzResolve` | `RK` 포함 |
|
||||
@@ -1,800 +0,0 @@
|
||||
# 실무협의문서 개발 가이드
|
||||
|
||||
> **대상 앱**: `frontend/apps/as`
|
||||
> **작성일**: 2026-08-27
|
||||
> **버전**: 1.0
|
||||
> **SoT (백엔드)**:
|
||||
>
|
||||
> - `PwrkCnsltnDocCrtRsltController` — `/api/v1/pwrk-cnsltn-doc-crt-rslt`
|
||||
> - `PwrkCnsltnMngController` — `/api/v1/pwrk-cnsltn-mng`
|
||||
> - `DcRagApiToController` — `/api/v1/dcrag-to`
|
||||
> - 초안생성 데이터 설명 (`02.03.초안생성_데이터_설명.md`)
|
||||
>
|
||||
> **현재 프론트 상태**: `pwrk-cnsltn-`\* 엔티티·화면 **미구현**. 재원협의(`fnrs-cnsltn`)만 존재한다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 문서 목적
|
||||
|
||||
재원협의 접수자가 **자료 검증 → 실무협의서 초안 생성 → 한글 웹기안기 열람/다운로드 → 편집본 버전 저장**까지 구현할 때 사용하는 프론트엔드 개발 가이드이다.
|
||||
|
||||
백엔드 컨트롤러·DTO를 **API source of truth**로 두고, 초안생성 데이터 설명의 업무 시퀀스를 화면 동작에 매핑한다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 도메인 개요
|
||||
|
||||
실무협의서(PWRK)는 재원협의서(FNRS)의 **하위 문서**이다. 문서번호 규칙:
|
||||
|
||||
| 문서 | 번호 예시 | 필드 |
|
||||
| -------- | ----------------- | ----------------------------- |
|
||||
| 재원협의 | `F-20260824-0001` | `fnrsCnsltnDocNo` / `upDocNo` |
|
||||
| 실무협의 | `P-20260827-0001` | `pwrkCnsltnDocNo` |
|
||||
|
||||
재원협의 1건당 실무협의서는 **1건만** 생성된다. `POST /pwrk-cnsltn-mng`는 `upDocNo`로 기존 문서를 재사용하고, 없으면 문서번호를 발급한다.
|
||||
|
||||
### 2-1. 핵심 식별자
|
||||
|
||||
| 필드 | 의미 | 비고 |
|
||||
| ----------------- | ------------------------------ | --------------------------------------------------------- |
|
||||
| `pwrkCnsltnDocNo` | 실무협의문서번호 | 문서 PK. `P-YYYYMMDD-NNNN` |
|
||||
| `upDocNo` | 상위문서번호(재원협의) | `upDocSeComCd = "FNRS"` |
|
||||
| `crtDmndUuid` | 생성요청 UUID | 초안 생성 작업·결과·다운로드의 키 |
|
||||
| `verSn` | 버전일련번호 | 복합 PK `(crtDmndUuid, verSn)`. 저장 시 서버가 `+1` |
|
||||
| `orgnlOtptDocCn` | 원본출력문서내용(HWP 바이너리) | **목록/상세 응답에서는 항상** `null`. 다운로드 API만 제공 |
|
||||
|
||||
### 2-2. IIPBS 약어
|
||||
|
||||
| 한글 | 약어 | 필드 예 |
|
||||
| -------- | ------ | ----------------------------- |
|
||||
| 실무 | PWRK | `pwrkCnsltnDocNo` |
|
||||
| 협의 | CNSLTN | `pwrk-cnsltn-mng` |
|
||||
| 관리 | MNG | `PwrkCnsltnMng` |
|
||||
| 문서 | DOC | `pwrkCnsltnDocNo` |
|
||||
| 생성 | CRT | `crtDmndUuid`, `crtSttsComCd` |
|
||||
| 요청 | DMND | `crtDmndUuid` |
|
||||
| 결과 | RSLT | `crtRslt` |
|
||||
| 버전 | VER | `verSn` |
|
||||
| 일련번호 | SN | `verSn` |
|
||||
| 원본 | ORGNL | `orgnlOtptDocCn` |
|
||||
| 출력 | OTPT | `orgnlOtptDocCn` |
|
||||
| 내용 | CN | `orgnlOtptDocCn` |
|
||||
| 완료 | CMPTN | `crtCmptnDt` |
|
||||
| 일시 | DT | `crtCmptnDt` |
|
||||
| 검증 | VRFC | `vrfcSttsComCd` |
|
||||
| 전송 | TRSM | `trsmFileList` |
|
||||
| 보완 | SPLMNT | `splmntDmndList` |
|
||||
| 요청자 | RQSTR | `rqstrUuid`, `rqstrNm` |
|
||||
|
||||
---
|
||||
|
||||
## 3. 전체 업무 흐름
|
||||
|
||||
재원협의 **접수자**가 실무협의서 초안을 만드는 경로이다. 프론트는 DC RAG를 직접 호출하지 않고 **as-service**만 호출한다.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant User as 재원협의 접수자
|
||||
participant FE as AS Frontend
|
||||
participant AS as as-service
|
||||
participant RAG as DC RAG
|
||||
|
||||
User->>FE: [자료 검증 요청]
|
||||
FE->>AS: POST /dcrag-to/documents/transfer<br/>{ fnrsCnsltnDocNo }
|
||||
AS->>RAG: 자료전송 (IF-01)
|
||||
AS-->>FE: { resultCode: SUCCESS, requestId }
|
||||
Note over FE: 재원협의 상세 재조회
|
||||
AS-->>FE: esntlFileList / etcFileList 에<br/>vrfcDt, vrfcRsltCn, vrfcSttsComCd
|
||||
|
||||
User->>FE: [초안 생성]
|
||||
FE->>AS: POST /pwrk-cnsltn-mng<br/>{ upDocNo, upDocSeComCd: FNRS, prgrsSttsComCd: DV }
|
||||
alt 실무협의서 번호 없음
|
||||
AS->>AS: P-번호 발급 + 문서 등록
|
||||
else 이미 존재
|
||||
AS->>AS: 기존 pwrkCnsltnDocNo 재사용
|
||||
end
|
||||
AS->>RAG: 초안생성 비동기 요청 (IF-03-1)
|
||||
AS-->>FE: 201 { pwrkCnsltnDocNo, crtDmndUuid, crtSttsComCd: R, estimatedMinutes: 10 }
|
||||
|
||||
loop 약 10분, status !== COMPLETED/FAILED
|
||||
FE->>AS: GET /dcrag-to/documents/draft/status/{crtDmndUuid}
|
||||
AS-->>FE: { status, events[], progressPercent }
|
||||
end
|
||||
|
||||
Note over FE: events 에 COMPLETED 가 있으면<br/>웹기안기 열기 / 다운로드 가능
|
||||
|
||||
User->>FE: [문서 열기] 또는 [다운로드]
|
||||
FE->>AS: GET /pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/webhwp/open<br/>또는 /download
|
||||
|
||||
User->>FE: 웹기안기 편집 후 [저장]
|
||||
FE->>AS: POST /pwrk-cnsltn-doc-crt-rslt (multipart)<br/>crtDmndUuid + file
|
||||
AS-->>FE: 201 { verSn: N+1, ... }
|
||||
```
|
||||
|
||||
### 3-1. 프론트가 호출하면 안 되는 API
|
||||
|
||||
| API | 이유 |
|
||||
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `POST /dcrag-to/documents/draft/generate` | 초안 생성은 `POST /pwrk-cnsltn-mng`가 문서번호 발급 + DC RAG 호출을 **한 트랜잭션**으로 처리한다. 프론트는 관리 API만 호출한다. |
|
||||
|
||||
---
|
||||
|
||||
## 4. 화면 데이터 흐름
|
||||
|
||||
초안 생성은 **재원협의 상세**에서 시작하고, 실무협의 목록/상세는 별도 화면이다.
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// ===== 재원협의 상세 (기존 fnrs-cnsltn view) =====
|
||||
CALL findById(fnrsCnsltnDocNo)
|
||||
DISPLAY 재원협의 상세 + 필수/기타 첨부
|
||||
|
||||
ON [자료 검증 요청] 버튼 CLICK DO
|
||||
CALL requestDocumentTransfer({ fnrsCnsltnDocNo })
|
||||
CALL findById(fnrsCnsltnDocNo) // 검증결과 필드 반영
|
||||
DISPLAY 첨부 리스트 (vrfcSttsComCd: PASS | FAIL)
|
||||
END ON
|
||||
|
||||
ON [초안 생성] 버튼 CLICK DO
|
||||
CALL pwrkCnsltnMngService.save({
|
||||
upDocNo: fnrsCnsltnDocNo,
|
||||
upDocSeComCd: "FNRS",
|
||||
prgrsSttsComCd: "DV"
|
||||
})
|
||||
// → pwrkCnsltnDocNo, crtDmndUuid, estimatedMinutes
|
||||
START POLL getDraftGenerationStatus(crtDmndUuid)
|
||||
DISPLAY 초안 생성 진행률 (약 10분)
|
||||
END ON
|
||||
|
||||
ON POLL status = COMPLETED 또는 events.eventType 에 COMPLETED DO
|
||||
ENABLE [웹기안기 열기] [다운로드]
|
||||
END ON
|
||||
|
||||
ON [웹기안기 열기] 버튼 CLICK DO
|
||||
CALL HwpCtrl.Open( webhwpOpenUrl(crtDmndUuid) ) // TBD: 컨트롤러 미구현
|
||||
END ON
|
||||
|
||||
ON [다운로드] 버튼 CLICK DO
|
||||
CALL downloadLatest(crtDmndUuid) // blob + Content-Disposition
|
||||
SAVE 로컬 파일
|
||||
END ON
|
||||
|
||||
// ===== 실무협의 목록 =====
|
||||
CALL pwrkCnsltnMngService.findPage(searchDto + pageable)
|
||||
DISPLAY 실무협의 문서 그리드
|
||||
|
||||
ON 행 CLICK DO
|
||||
NAVIGATE 실무협의 상세 (pwrkCnsltnDocNo)
|
||||
END ON
|
||||
|
||||
// ===== 실무협의 상세 / 재원협의에서 연계 조회 =====
|
||||
CALL findById(pwrkCnsltnDocNo)
|
||||
// 또는 재원협의 번호로: findByUpDocNo(fnrsCnsltnDocNo)
|
||||
DISPLAY 마스터 + cnsltnList + pjtcoAjmtList + crtDmndList.crtRslt
|
||||
// crtRslt.orgnlOtptDocCn 은 항상 null — 바이너리는 download API
|
||||
|
||||
ON 버전 이력 조회 DO
|
||||
CALL pwrkCnsltnDocCrtRsltService.findPage({ crtDmndUuid, sort: "verSn,desc" })
|
||||
DISPLAY 버전 목록
|
||||
END ON
|
||||
|
||||
ON 웹기안기 편집 완료 [저장] DO
|
||||
CALL saveMultipart({ crtDmndUuid, file }) // 서버가 verSn + 1
|
||||
INVALIDATE 상세·버전 목록
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. API 명세
|
||||
|
||||
### 5-1. 공통
|
||||
|
||||
| 항목 | 값 |
|
||||
| ----------- | ----------------------------------------------------------------- |
|
||||
| Gateway | `/as-service` |
|
||||
| Base prefix | `ENDPOINT.AS_SERVICE.*` (`apps/as/src/shared/config/api.ts`) |
|
||||
| Auth 헤더 | `Authorization`, `X-Site-Id`, `token-id` (기존 `client` 인터셉터) |
|
||||
| 날짜 포맷 | `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
추가할 ENDPOINT:
|
||||
|
||||
```ts
|
||||
AS_SERVICE: {
|
||||
// 기존 …
|
||||
PWRK_CNSLTN_MNG: "/as-service/api/v1/pwrk-cnsltn-mng",
|
||||
PWRK_CNSLTN_DOC_CRT_RSLT: "/as-service/api/v1/pwrk-cnsltn-doc-crt-rslt",
|
||||
DCRAG_TO: "/as-service/api/v1/dcrag-to",
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5-2. DC RAG 연계 (`DcRagApiToController`)
|
||||
|
||||
프론트 엔티티 슬라이스: `entities/dcrag-to/`
|
||||
|
||||
| 기능 | Controller | Method | URL | Request | Response |
|
||||
| ----------------- | -------------------------- | ------ | ------------------------------------------------ | --------------------------------- | ------------------------------------- |
|
||||
| 자료전송요청 | `requestDocumentTransfer` | POST | `/dcrag-to/documents/transfer` | `DcRagDocumentTransferRequestDto` | `DcRagCommonResponseDto` |
|
||||
| 초안생성 진행상태 | `getDraftGenerationStatus` | GET | `/dcrag-to/documents/draft/status/{crtDmndUuid}` | path | `DcRagDraftStatusResponseDto` |
|
||||
| 금액검증요청 | `requestCostVerification` | POST | `/dcrag-to/documents/cost/verify` | `DcRagCostVerifyRequestDto` | `DcRagCommonResponseDto` (202) |
|
||||
| 초안생성요청 | `requestDraftGeneration` | POST | `/dcrag-to/documents/draft/generate` | `DcRagDraftGenerateRequestDto` | `DcRagDraftGenerateResponseDto` (202) |
|
||||
|
||||
**화면에서 쓰는 것**: 자료전송 + 상태 폴링 (+ 추후 금액검증).
|
||||
초안생성요청은 5-3의 `pwrkCnsltnMngService.save`로 대체한다.
|
||||
|
||||
#### 자료전송 Request / Response
|
||||
|
||||
```json
|
||||
POST /as-service/api/v1/dcrag-to/documents/transfer
|
||||
{ "fnrsCnsltnDocNo": "F-20260824-0001" }
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"resultCode": "SUCCESS",
|
||||
"message": "자료전송요청 접수 완료",
|
||||
"requestId": "F-20260824-0001"
|
||||
}
|
||||
```
|
||||
|
||||
자료 검증이 **완료된 뒤** 재원협의 상세를 다시 조회하면, 필수첨부(`esntlFileList`)·기타첨부(`etcFileList`) 각 파일에 검증 결과가 붙는다.
|
||||
|
||||
| 필드 | 예 | 의미 |
|
||||
| --------------- | ----------------------------- | ------------ |
|
||||
| `vrfcDt` | `2026-08-25 18:13:49` | 검증일시 |
|
||||
| `vrfcRsltCn` | `정상 확인 (재원협의 신청서)` | 검증결과내용 |
|
||||
| `vrfcSttsComCd` | `PASS` / `FAIL` | 검증상태 |
|
||||
|
||||
프론트 `FnrsCnsltnMngDetResponseDto` / `AtflMngClientResponseDto` / `EsntlAtchFileMngTreeResponseDto`에 위 3필드가 **아직 없다**. 재원협의 상세 첨부 영역에 배지를 그리려면 타입을 확장해야 한다.
|
||||
|
||||
#### 초안생성 상태 폴링
|
||||
|
||||
```http
|
||||
GET /as-service/api/v1/dcrag-to/documents/draft/status/{crtDmndUuid}
|
||||
```
|
||||
|
||||
| 필드 | 설명 |
|
||||
| ----------------------- | --------------------------------------------------------------------------------- |
|
||||
| `status` | `REQUESTED` / `DOWNLOADING` / `ANALYZING` / `GENERATING` / `COMPLETED` / `FAILED` |
|
||||
| `statusName` | 표시명 (예: `생성 완료`) |
|
||||
| `progressPercent` | 0~100 |
|
||||
| `events[]` | `{ eventType, eventMessage, occurredDt }` |
|
||||
| `startedDt` | 시작일시 |
|
||||
| `estimatedCompletionDt` | 예상 완료일시 |
|
||||
| `errorMessage` | 실패 시 메시지 |
|
||||
|
||||
폴링 규칙:
|
||||
|
||||
- 주기: **5~10초** (TBD, UX 확정).
|
||||
- 예상 소요: **약 10분** (`estimatedMinutes`).
|
||||
- 종료 조건: `status === "COMPLETED"` **또는** `events`에 `eventType === "COMPLETED"`.
|
||||
- 실패: `status === "FAILED"` → 폴링 중단, `errorMessage` 표시.
|
||||
- 완료 전에는 웹기안기 열기·다운로드 버튼을 **비활성**.
|
||||
|
||||
이벤트 순서 예시:
|
||||
|
||||
| eventType | eventMessage |
|
||||
| ------------- | --------------------------- |
|
||||
| `REQUESTED` | 요청을 접수했습니다. |
|
||||
| `DOWNLOADING` | 신청서를 수신하는 중입니다. |
|
||||
| `ANALYZING` | 자료 준비가 완료되었습니다. |
|
||||
| `GENERATING` | 문서 저장이 완료되었습니다. |
|
||||
| `COMPLETED` | 문서 생성이 완료되었습니다. |
|
||||
|
||||
---
|
||||
|
||||
### 5-3. 실무협의관리 (`PwrkCnsltnMngController`)
|
||||
|
||||
프론트 엔티티 슬라이스: `entities/pwrk-cnsltn-mng/`
|
||||
|
||||
| 기능 | Controller | Method | URL | Request | Response |
|
||||
| ------------------- | ---------------------- | ------ | ----------------------------------------------------- | ----------------------------------- | ------------------------------------ |
|
||||
| 페이징 조회 | `findPage` | GET | `/pwrk-cnsltn-mng` | `PwrkCnsltnMngSearchDto` + Pageable | `Page<PwrkCnsltnMngResponseDto>` |
|
||||
| 상세 (문서번호) | `findById` | GET | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}` | path | `PwrkCnsltnMngDetResponseDto` |
|
||||
| 상세 (재원협의번호) | `findByUpDocNo` | GET | `/pwrk-cnsltn-mng/updocno/{upDocNo}` | path | `PwrkCnsltnMngDetResponseDto` |
|
||||
| 등록 + 초안생성 | `save` | POST | `/pwrk-cnsltn-mng` | `PwrkCnsltnMngSaveRequestDto` | `PwrkCnsltnMngSaveResponseDto` (201) |
|
||||
| 신청 통합 저장 | `saveApply` | POST | `/pwrk-cnsltn-mng/save-apply` | `PwrkCnsltnApplySaveRequestDto` | `string` (문서번호, 201) |
|
||||
| 수정 | `update` | PUT | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}` | `PwrkCnsltnMngUpdateRequestDto` | `string` |
|
||||
| 삭제 | `delete` → `remove` | DELETE | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}` | path | 204 |
|
||||
| 보완완료 제출 | `submitSupplement` | POST | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}/submitsupplement` | `PwrkCnsltnApplySaveRequestDto` | `string` |
|
||||
| 보완요청 등록 | `submitReqSupplement` | POST | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}/reqsupplement` | `PwrkCnsltnApplySaveRequestDto` | `string` |
|
||||
| 진행상태 수정 | `changePrgrsSttsComCd` | POST | `/pwrk-cnsltn-mng/doc/updatePrgrsStts` | `DocPrgrsUpdateRequestDto` | 200 |
|
||||
|
||||
목록 기본 호출:
|
||||
|
||||
```http
|
||||
GET /as-service/api/v1/pwrk-cnsltn-mng?page=0&size=10&sort=pwrkCnsltnDocNo,desc
|
||||
```
|
||||
|
||||
#### 초안 생성 Request / Response (`save`)
|
||||
|
||||
```json
|
||||
POST /as-service/api/v1/pwrk-cnsltn-mng
|
||||
{
|
||||
"upDocNo": "F-20260824-0001",
|
||||
"upDocSeComCd": "FNRS",
|
||||
"prgrsSttsComCd": "DV"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"pwrkCnsltnDocNo": "P-20260826-0001",
|
||||
"upDocNo": "F-20260824-0001",
|
||||
"upDocSeComCd": "FNRS",
|
||||
"prgrsSttsComCd": "DV",
|
||||
"resultCode": "ACCEPTED",
|
||||
"message": "초안 생성 작업이 접수되었습니다.",
|
||||
"crtDmndUuid": "382f2821-9231-42e6-8309-4a1718a2ead8",
|
||||
"crtSttsComCd": "R",
|
||||
"requestId": "382f2821-9231-42e6-8309-4a1718a2ead8",
|
||||
"estimatedMinutes": 10
|
||||
}
|
||||
```
|
||||
|
||||
백엔드 동작 (`PwrkCnsltnMngService.save`):
|
||||
|
||||
1. `upDocNo`로 기존 실무협의서가 있으면 **그 번호를 재사용** (재원협의 1 : 실무협의 1).
|
||||
2. 없으면 `docNoMngService.issueDocNo(PWRK)`로 `P-` 번호 발급 후 insert.
|
||||
3. DC RAG 초안생성을 비동기로 호출하고 `crtDmndUuid`를 응답에 실어 반환.
|
||||
4. DC RAG 실패 시에도 **문서는 롤백하지 않고** 로그만 남긴다. 프론트는 `resultCode`가 `ACCEPTED`/`SUCCESS`가 아니면 토스트로 실패를 알린다.
|
||||
|
||||
#### 상세 응답 구조 (`PwrkCnsltnMngDetResponseDto`)
|
||||
|
||||
재원협의 상세에서 `GET .../updocno/{fnrsCnsltnDocNo}`로 연계 데이터를 한 번에 가져온다.
|
||||
|
||||
```json
|
||||
{
|
||||
"pwrkCnsltnDocNo": "P-20260827-0001",
|
||||
"upDocNo": "F-20260824-0001",
|
||||
"upDocSeComCd": "FNRS",
|
||||
"prgrsSttsComCd": "DV",
|
||||
"cnsltnList": [],
|
||||
"pjtcoAjmtList": [],
|
||||
"crtDmndList": [
|
||||
{
|
||||
"crtDmndUuid": "d0545bef-735f-4d7b-a781-88e96e03577a",
|
||||
"pwrkCnsltnDocNo": "P-20260827-0001",
|
||||
"rqstrUuid": "65a00f65-8460-49af-98ec-042977e56f4b",
|
||||
"rqstrNm": "관리자",
|
||||
"crtSttsComCd": "C",
|
||||
"crtRslt": {
|
||||
"crtDmndUuid": "d0545bef-735f-4d7b-a781-88e96e03577a",
|
||||
"verSn": 1,
|
||||
"rgtrUuid": "...",
|
||||
"rgtrNm": "관리자",
|
||||
"orgnlOtptDocCn": null,
|
||||
"crtCmptnDt": "2026-08-27 10:47:01"
|
||||
}
|
||||
}
|
||||
],
|
||||
"splmntDmndList": [],
|
||||
"trsmFileList": []
|
||||
}
|
||||
```
|
||||
|
||||
`crtRslt`는 최신 버전 메타만 포함한다. `orgnlOtptDocCn`은 절대 쓰지 말고 download API를 호출한다.
|
||||
|
||||
마스터에 재원협의 사업정보가 조인된다: `bizNm`, `admdstCd`, `envoSeComCd`, `taskSeComCd`, `aplcntNm` 등.
|
||||
|
||||
---
|
||||
|
||||
### 5-4. 문서생성결과 (`PwrkCnsltnDocCrtRsltController`)
|
||||
|
||||
프론트 엔티티 슬라이스: `entities/pwrk-cnsltn-doc-crt-rslt/`
|
||||
복합 PK: `crtDmndUuid` + `verSn`
|
||||
|
||||
| 기능 | Controller | Method | URL | Request | Response |
|
||||
| ------------------ | ------------------------- | ------ | ---------------------------------- | ----------------------------------------------- | --------------------------------------- |
|
||||
| 버전 이력 페이징 | `findPage` | GET | `/pwrk-cnsltn-doc-crt-rslt` | `PwrkCnsltnDocCrtRsltSearchDto` + Pageable | `Page<PwrkCnsltnDocCrtRsltResponseDto>` |
|
||||
| 특정 버전 상세 | `findById` | GET | `/{crtDmndUuid}/{verSn}` | path | `PwrkCnsltnDocCrtRsltResponseDto` |
|
||||
| 최신 버전 상세 | `findLatestByCrtDmndUuid` | GET | `/{crtDmndUuid}/latest` | path | `PwrkCnsltnDocCrtRsltResponseDto` |
|
||||
| 특정 버전 다운로드 | `download` | GET | `/{crtDmndUuid}/{verSn}/download` | path | `byte[]` (attachment) |
|
||||
| 최신 버전 다운로드 | `downloadLatest` | GET | `/{crtDmndUuid}/download` | path | `byte[]` (attachment) |
|
||||
| 등록 (multipart) | `saveMultipart` | POST | `/` `multipart/form-data` | `PwrkCnsltnDocCrtRsltSaveRequestDto` + `file` | `PwrkCnsltnDocCrtRsltResponseDto` (201) |
|
||||
| 등록 (JSON) | `saveJson` | POST | `/` `application/json` | `PwrkCnsltnDocCrtRsltSaveRequestDto` | 201 |
|
||||
| 수정 (multipart) | `updateMultipart` | PUT | `/{crtDmndUuid}/{verSn}` multipart | `PwrkCnsltnDocCrtRsltUpdateRequestDto` + `file` | ResponseDto |
|
||||
| 수정 (JSON) | `updateJson` | PUT | `/{crtDmndUuid}/{verSn}` json | UpdateRequestDto | ResponseDto |
|
||||
| 삭제 | `delete` → `remove` | DELETE | `/{crtDmndUuid}/{verSn}` | path | 204 |
|
||||
|
||||
Spring 매핑 주의: `/{crtDmndUuid}/latest` 와 `/{crtDmndUuid}/download` 는 `/{crtDmndUuid}/{verSn}` 보다 **구체 경로가 우선**한다. 프론트 path를 섞지 말 것.
|
||||
|
||||
버전 이력 조회 예:
|
||||
|
||||
```http
|
||||
GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt?page=0&size=10&crtDmndUuid={uuid}&sort=verSn,desc
|
||||
```
|
||||
|
||||
#### 다운로드 (attachment)
|
||||
|
||||
화면 [다운로드] 버튼 → PC 저장.
|
||||
|
||||
```http
|
||||
GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/download
|
||||
GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/{verSn}/download
|
||||
```
|
||||
|
||||
- axios: `responseType: "blob"`.
|
||||
- 파일명: `Content-Disposition`의 `filename*=UTF-8''...` (`parseFilenameFromResponse` 재사용).
|
||||
- 서버 파일명 규칙: `재원협의 실무협의서({bizNm})_v{verSn}.hwp`.
|
||||
- **현재 컨트롤러 Content-Type**: `application/x-hwp`.
|
||||
데이터 설명 예시는 `application/haansofthwpx`. 프론트는 Content-Type에 의존하지 말고 blob으로 저장한다.
|
||||
|
||||
기존 `attachService` 다운로드와 동일 패턴:
|
||||
|
||||
```ts
|
||||
const response = await client.get<Blob>(url, { responseType: "blob" });
|
||||
const filename = parseFilenameFromResponse(response);
|
||||
const objectUrl = window.URL.createObjectURL(response.data);
|
||||
```
|
||||
|
||||
#### 웹기안기 열기 (inline) — TBD
|
||||
|
||||
데이터 설명 기준 엔드포인트. **현재** `PwrkCnsltnDocCrtRsltController`**에는 없음.** 백엔드 추가 후 연결한다.
|
||||
|
||||
```http
|
||||
GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/webhwp/open
|
||||
GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/{verSn}/webhwp/open
|
||||
```
|
||||
|
||||
의도:
|
||||
|
||||
- `Content-Disposition: inline`
|
||||
- 한컴 웹기안기 `HwpCtrl.Open(url)` 이 이 URL을 직접 호출
|
||||
- 브라우저 다운로드 창이 뜨지 않고 캔버스에 로드
|
||||
|
||||
프론트는 **blob fetch 후 Object URL을 넘기지 않는다.** 인증 헤더가 필요한 절대 URL을 기안기에 전달하는 방식이므로, 쿠키/게이트웨이 인증 가능 여부를 백엔드와 확인한다 (TBD).
|
||||
|
||||
#### 신규 버전 등록 (웹기안기 저장)
|
||||
|
||||
서버가 최신 `verSn + 1`을 부여한다. 클라이언트가 `verSn`을 보내지 않는다.
|
||||
|
||||
```http
|
||||
POST /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
crtDmndUuid: {uuid}
|
||||
file: 재원협의 실무협의서_v2.hwpx
|
||||
```
|
||||
|
||||
화면에서는 **multipart만** 사용한다. JSON `saveJson`은 바이너리를 body에 넣는 경로라 UI에서 쓰지 않는다.
|
||||
|
||||
`file` part 이름: `"file"`. DTO 필드는 `@ModelAttribute` 로 평문 필드(`crtDmndUuid`) 또는 `saveDto` part.
|
||||
|
||||
---
|
||||
|
||||
## 6. DTO 매핑 (프론트 타입명 = 백엔드 클래스명)
|
||||
|
||||
### 6-1. `PwrkCnsltnDocCrtRslt*`
|
||||
|
||||
| 계층 | 파일 | 정의 |
|
||||
| ------------------------------------ | -------------------------------------------- | ----------------------------------------------- |
|
||||
| Save Request | `pwrk-cnsltn-doc-crt-rslt-payload-schema.ts` | zod SSOT → `PwrkCnsltnDocCrtRsltSaveRequestDto` |
|
||||
| Response / Search / Update / Service | `pwrk-cnsltn-doc-crt-rslt-interface.ts` | `interface` |
|
||||
|
||||
**Response**
|
||||
|
||||
| 필드 | 타입 | 비고 |
|
||||
| ---------------- | -------- | --------------------- |
|
||||
| `crtDmndUuid` | `string` | UUID |
|
||||
| `verSn` | `number` | BigDecimal |
|
||||
| `rgtrUuid` | `string` | |
|
||||
| `rgtrNm` | `string` | 서버 enrich |
|
||||
| `orgnlOtptDocCn` | `null` | 목록/상세 미포함 |
|
||||
| `crtCmptnDt` | `string` | `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
**Search** (`PageableParams` 확장)
|
||||
|
||||
| 필드 | 비고 |
|
||||
| ----------------- | ---------------------- |
|
||||
| `crtDmndUuid` | 버전 이력 조회 시 필수 |
|
||||
| `pwrkCnsltnDocNo` | |
|
||||
| `verSn` | |
|
||||
| `rgtrUuid` | |
|
||||
|
||||
**Save Request (zod)**
|
||||
|
||||
| 필드 | 필수 | 비고 |
|
||||
| ------------- | ---- | ------------------------------------ |
|
||||
| `crtDmndUuid` | Y | |
|
||||
| `rgtrUuid` | N | 미전송 시 서버가 로그인 사용자 사용 |
|
||||
| `file` | UI | multipart `file` part. DTO 필드 아님 |
|
||||
|
||||
**Update Request**
|
||||
|
||||
| 필드 | 비고 |
|
||||
| ------------ | --------- |
|
||||
| `rgtrUuid` | |
|
||||
| `crtCmptnDt` | |
|
||||
| `file` | multipart |
|
||||
|
||||
### 6-2. `PwrkCnsltnMng*`
|
||||
|
||||
**Search**
|
||||
|
||||
`pwrkCnsltnDocNo`, `upDocNo`, `upDocSeComCd`, `prgrsSttsComCd`, `admdstCd`, `envoSeComCd`, `taskSeComCd`, `bizNm`
|
||||
|
||||
**Save Request (초안 생성)** — 필드가 얇으므로 interface만으로도 가능. 권장은 zod.
|
||||
|
||||
| 필드 | 필수 | 값 |
|
||||
| ----------------- | ---- | ----------------- |
|
||||
| `upDocNo` | Y | 재원협의 문서번호 |
|
||||
| `upDocSeComCd` | | `"FNRS"` |
|
||||
| `prgrsSttsComCd` | | `"DV"` (초안생성) |
|
||||
| `pwrkCnsltnDocNo` | N | 서버 발급 |
|
||||
|
||||
**Save Response** — `save`만 이 DTO. `fnrsCnsltnMngService.save`가 `string`을 반환하는 것과 **다름**.
|
||||
|
||||
| 필드 | 의미 |
|
||||
| ------------------ | --------------------------------- |
|
||||
| `pwrkCnsltnDocNo` | 발급/재사용 번호 |
|
||||
| `resultCode` | `ACCEPTED` / `SUCCESS` / 실패코드 |
|
||||
| `crtDmndUuid` | 폴링 키 |
|
||||
| `crtSttsComCd` | `R` 생성요청 |
|
||||
| `estimatedMinutes` | 기본 10 |
|
||||
|
||||
**DetResponse 중첩**
|
||||
|
||||
- `cnsltnList`: 협의내역 (`rvwOpnnSn`, `rvwOpnnClsfComCd`, `cnsltnCn`, `rfltYn`, …)
|
||||
- `pjtcoAjmtList`: 사업비조정
|
||||
- `crtDmndList[].crtRslt`: 생성요청 + 최신 결과
|
||||
- `splmntDmndList`: 보완요청
|
||||
- `sttsChgHstryList`: 상태변경이력
|
||||
- `trsmFileList`: 전송파일 (데이터 설명 기준. Det DTO Java 필드 확인 후 타입 반영)
|
||||
|
||||
### 6-3. DC RAG
|
||||
|
||||
```ts
|
||||
interface DcRagDocumentTransferRequestDto {
|
||||
fnrsCnsltnDocNo: string;
|
||||
taskSeComCd?: string;
|
||||
taskSeCdNm?: string;
|
||||
}
|
||||
|
||||
interface DcRagCommonResponseDto {
|
||||
resultCode: string;
|
||||
message?: string;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
interface DcRagDraftStatusResponseDto {
|
||||
crtDmndUuid: string;
|
||||
status: DraftStatus;
|
||||
statusName?: string;
|
||||
progressPercent?: number;
|
||||
events?: EventInfoDto[];
|
||||
startedDt?: string;
|
||||
estimatedCompletionDt?: string;
|
||||
errorMessage?: string | null;
|
||||
}
|
||||
|
||||
type DraftStatus =
|
||||
| "REQUESTED"
|
||||
| "DOWNLOADING"
|
||||
| "ANALYZING"
|
||||
| "GENERATING"
|
||||
| "COMPLETED"
|
||||
| "FAILED";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 공통코드
|
||||
|
||||
| 상위코드 | 필드 | 값 | 명칭 |
|
||||
| ----------------------- | ---------------- | --------------- | ------------- |
|
||||
| `ASDOCSE` | `upDocSeComCd` | `FNRS` | 재원협의 |
|
||||
| `ASDOCSE` | 문서구분 | `PWRK` | 실무협의 |
|
||||
| `PWRKSTTS` | `prgrsSttsComCd` | `DV` | 초안생성 |
|
||||
| `PWRKSTTS` | | `AR` | 검토자지정 |
|
||||
| `PWRKSTTS` | | `SV` | 작성중 |
|
||||
| `PWRKSTTS` | | `RB` | 검토회신 |
|
||||
| `PWRKSTTS` | | `RC` | 협의회신 |
|
||||
| `PWRKSTTS` | | `TR` | 검토결과 회신 |
|
||||
| `PWRKSTTS` | | `SL` | 보완요청 |
|
||||
| `PWRKSTTS` | | `AA` | 금액조정중 |
|
||||
| `AICRTSTTS` / `CRTSTTS` | `crtSttsComCd` | `R` | 생성요청 |
|
||||
| | | `I` | 생성중 |
|
||||
| | | `C` | 생성완료 |
|
||||
| | | `F` | 생성실패 |
|
||||
| (검증) | `vrfcSttsComCd` | `PASS` / `FAIL` | |
|
||||
|
||||
`CrtDmndResponseDto`는 `@CodeName(upCd = "CRTSTTS")`, 생성요청 단건 DTO는 `AICRTSTTS`를 쓴다. 코드값이 `R/I/C/F`로 같으므로 UI는 코드값으로 분기하고, 표시명은 응답의 `*Nm` 필드를 쓴다.
|
||||
|
||||
초안 생성 직후 `crtSttsComCd = R`, 폴링 `COMPLETED` 이후 상세의 `crtSttsComCd = C`.
|
||||
|
||||
---
|
||||
|
||||
## 8. 프론트엔드 구현 가이드
|
||||
|
||||
### 8-1. 기술 스택·원칙
|
||||
|
||||
- App Router + FSD (`app/` → `pages/` → `widgets/` → `entities/` → `shared/`)
|
||||
- React Query. ad-hoc `useQuery` + service 직접 호출 금지
|
||||
- `@common/components`, `useDataTable`, `@tanstack/react-table`
|
||||
- `'use client'`는 Web API(다운로드, 폴링, 웹기안기)가 있는 페이지만
|
||||
- 함수명 = Controller public 메서드명 (`delete` → `remove`)
|
||||
- 타입명 = 백엔드 DTO명. 프론트 전용 축약 금지
|
||||
- Save/Apply Request = zod SSOT. Response는 `interface`
|
||||
|
||||
참조 슬라이스: `entities/fnrs-cnsltn/`, `entities/sewer-bsc-plan/`
|
||||
|
||||
### 8-2. 모듈·파일 배치
|
||||
|
||||
| 구분 | 경로 |
|
||||
| -------------------- | ------------------------------------------------------------------- |
|
||||
| ENDPOINT | `apps/as/src/shared/config/api.ts` |
|
||||
| 실무협의 관리 엔티티 | `entities/pwrk-cnsltn-mng/` |
|
||||
| 생성결과 엔티티 | `entities/pwrk-cnsltn-doc-crt-rslt/` |
|
||||
| DC RAG 엔티티 | `entities/dcrag-to/` |
|
||||
| 목록 라우트 | `app/(main)/(with-base-layout)/pwrk-cnsltn/page.tsx` |
|
||||
| 상세 라우트 | `app/(main)/(with-base-layout)/pwrk-cnsltn/[id]/page.tsx` |
|
||||
| 페이지 UI | `pages/pwrk-cnsltn/` |
|
||||
| 재원협의 상세 연계 | `pages/fnrs-cnsltn/ui/fnrs-view-page.tsx` (버튼·폴링·첨부 검증배지) |
|
||||
|
||||
엔티티 내부:
|
||||
|
||||
```
|
||||
entities/pwrk-cnsltn-mng/
|
||||
├── model/
|
||||
│ ├── pwrk-cnsltn-mng-interface.ts
|
||||
│ └── pwrk-cnsltn-mng-payload-schema.ts
|
||||
├── api/
|
||||
│ ├── pwrk-cnsltn-mng-service.ts
|
||||
│ └── queries.ts
|
||||
└── index.ts
|
||||
|
||||
entities/pwrk-cnsltn-doc-crt-rslt/
|
||||
├── model/
|
||||
│ ├── pwrk-cnsltn-doc-crt-rslt-interface.ts
|
||||
│ └── pwrk-cnsltn-doc-crt-rslt-payload-schema.ts
|
||||
├── api/
|
||||
│ ├── pwrk-cnsltn-doc-crt-rslt-service.ts
|
||||
│ └── queries.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
### 8-3. Service 시그니처 요지
|
||||
|
||||
`pwrkCnsltnMngService.save` 반환 타입이 `string`이 아니라 `PwrkCnsltnMngSaveResponseDto` 인 점을 `fnrs-cnsltn`과 혼동하지 말 것.
|
||||
|
||||
```ts
|
||||
export interface PwrkCnsltnMngService {
|
||||
findPage(searchDto: PwrkCnsltnMngSearchDto): Promise<PageResponse<PwrkCnsltnMngResponseDto>>;
|
||||
findById(pwrkCnsltnDocNo: string): Promise<PwrkCnsltnMngDetResponseDto>;
|
||||
findByUpDocNo(upDocNo: string): Promise<PwrkCnsltnMngDetResponseDto>;
|
||||
save(body: PwrkCnsltnMngSaveRequestDto): Promise<PwrkCnsltnMngSaveResponseDto>;
|
||||
saveApply(body: PwrkCnsltnApplySaveRequestDto): Promise<string>;
|
||||
update(pwrkCnsltnDocNo: string, body: PwrkCnsltnMngUpdateRequestDto): Promise<string>;
|
||||
submitSupplement(pwrkCnsltnDocNo: string, body: PwrkCnsltnApplySaveRequestDto): Promise<string>;
|
||||
submitReqSupplement(
|
||||
pwrkCnsltnDocNo: string,
|
||||
body: PwrkCnsltnApplySaveRequestDto,
|
||||
): Promise<string>;
|
||||
remove(pwrkCnsltnDocNo: string): Promise<void>;
|
||||
changePrgrsSttsComCd(body: DocPrgrsUpdateRequestDto): Promise<void>;
|
||||
}
|
||||
|
||||
export interface PwrkCnsltnDocCrtRsltService {
|
||||
findPage(
|
||||
searchDto: PwrkCnsltnDocCrtRsltSearchDto,
|
||||
): Promise<PageResponse<PwrkCnsltnDocCrtRsltResponseDto>>;
|
||||
findById(crtDmndUuid: string, verSn: number): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
|
||||
findLatestByCrtDmndUuid(crtDmndUuid: string): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
|
||||
download(crtDmndUuid: string, verSn: number): Promise<AxiosResponse<Blob>>;
|
||||
downloadLatest(crtDmndUuid: string): Promise<AxiosResponse<Blob>>;
|
||||
saveMultipart(formData: FormData): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
|
||||
updateMultipart(
|
||||
crtDmndUuid: string,
|
||||
verSn: number,
|
||||
formData: FormData,
|
||||
): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
|
||||
remove(crtDmndUuid: string, verSn: number): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
multipart `Content-Type`은 axios가 boundary를 붙이도록 **헤더를 수동 지정하지 않는다.**
|
||||
|
||||
### 8-4. 폴링 훅
|
||||
|
||||
`useQuery` + `refetchInterval`:
|
||||
|
||||
```ts
|
||||
useQuery({
|
||||
queryKey: [...dcragToKeys.draftStatus(crtDmndUuid)],
|
||||
queryFn: () => dcragToService.getDraftGenerationStatus(crtDmndUuid),
|
||||
enabled: Boolean(crtDmndUuid) && isPolling,
|
||||
refetchInterval: (query) => {
|
||||
const status = query.state.data?.status;
|
||||
if (status === "COMPLETED" || status === "FAILED") return false;
|
||||
return 5_000;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`COMPLETED` 시 `pwrkCnsltnMng` 상세 쿼리를 invalidate 하여 `crtDmndList[].crtRslt`를 갱신한다.
|
||||
|
||||
### 8-5. 재원협의 상세에 붙일 UI
|
||||
|
||||
`pages/fnrs-cnsltn` 조회 모드:
|
||||
|
||||
1. **자료 검증 요청** — `requestDocumentTransfer` → 상세 refetch → 첨부 행에 PASS/FAIL 배지 (`vrfcSttsComCd`, `vrfcRsltCn`, `vrfcDt`).
|
||||
2. **초안 생성** — 검증이 끝난 뒤 활성. `pwrkCnsltnMngService.save`.
|
||||
3. **진행 표시** — `progressPercent` + 이벤트 타임라인. `estimatedMinutes`(10)를 안내.
|
||||
4. **웹기안기 열기 / 다운로드** — `COMPLETED` 이후에만.
|
||||
|
||||
`findByUpDocNo(fnrsCnsltnDocNo)`로 이미 초안이 있으면 `crtDmndUuid`를 복원해 폴링·열기를 이어간다.
|
||||
|
||||
### 8-6. 실무협의 목록 컬럼 (권장)
|
||||
|
||||
마스터 응답 + 재원협의 조인 필드 기준. 화면설계 확정 전 TBD.
|
||||
|
||||
| 표시명 | Field |
|
||||
| ------------ | ------------------ |
|
||||
| 실무협의번호 | `pwrkCnsltnDocNo` |
|
||||
| 재원협의번호 | `upDocNo` |
|
||||
| 진행상태 | `prgrsSttsComCdNm` |
|
||||
| 사업명 | `bizNm` |
|
||||
| 시도/구군 | `sido` / `gugun` |
|
||||
| 신청자 | `aplcntNm` |
|
||||
| 등록일 | `regDt` |
|
||||
|
||||
행 클릭 → `/pwrk-cnsltn/{pwrkCnsltnDocNo}`.
|
||||
|
||||
---
|
||||
|
||||
## 9. 구현 순서
|
||||
|
||||
```
|
||||
Task Progress:
|
||||
- [ ] 1. ENDPOINT 3개 추가 (PWRK_CNSLTN_MNG, PWRK_CNSLTN_DOC_CRT_RSLT, DCRAG_TO)
|
||||
- [ ] 2. entities/dcrag-to (transfer + draft status)
|
||||
- [ ] 3. entities/pwrk-cnsltn-mng (findPage, findById, findByUpDocNo, save)
|
||||
- [ ] 4. entities/pwrk-cnsltn-doc-crt-rslt (findPage, downloadLatest, saveMultipart)
|
||||
- [ ] 5. 재원협의 상세: 검증결과 3필드 타입 확장 + 배지
|
||||
- [ ] 6. 재원협의 상세: 자료검증 / 초안생성 / 폴링 UI
|
||||
- [ ] 7. 실무협의 목록·상세 라우트
|
||||
- [ ] 8. 다운로드 (blob)
|
||||
- [ ] 9. 웹기안기 Open — 백엔드 webhwp/open 확인 후
|
||||
- [ ] 10. 웹기안기 저장 → saveMultipart (verSn + 1)
|
||||
```
|
||||
|
||||
1~4는 엔티티 레이어만으로 먼저 닫는다. 화면은 5부터.
|
||||
|
||||
---
|
||||
|
||||
## 10. TBD · 백엔드 확인
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `webhwp/open` | 데이터 설명에는 있으나 **현재 Controller에 없음**. 추가 일정·인증 방식 확인 |
|
||||
| Content-Type | Controller `application/x-hwp` vs 설명 `application/haansofthwpx` |
|
||||
| 파일 확장자 | 다운로드 파일명이 `.hwp`로 고정. 실제 바이너리가 hwpx일 수 있음 |
|
||||
| 폴링 주기 | 5초 vs 10초, 탭 비활성 시 중단 여부 |
|
||||
| 검증 필드 타입 | `FnrsCnsltnMngDetResponseDto.esntlFileList` / `etcFileList` Java 타입이 아직 `FnrsCnsltnAtflResponseDto`가 아님. 런타임 JSON에 `vrfc*`가 오는지 확인 |
|
||||
| `trsmFileList` | 데이터 설명 상세 예시에는 있으나 Det DTO Java 선언과 불일치 가능 |
|
||||
| `crtSttsComCd` 상위코드 | `CRTSTTS` vs `AICRTSTTS` |
|
||||
| 웹기안기 라이선스 URL | `HwpCtrl.Open`에 넘길 게이트웨이 절대 경로 (쿠키 인증 가능 여부) |
|
||||
| 초안 생성 1회 제한 | `requestDraftGeneration` 주석에 “1회 제한”. 재클릭 UX (비활성 / 재요청 확인) |
|
||||
| 자료전송 허용 확장자 | `.doc .docx .hwp .hwpx .md .pdf .txt .xls .xlsm .xlsx`. 이미지 첨부 시 DC RAG 실패 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 참고
|
||||
|
||||
| 자료 | 위치 |
|
||||
| ---------------------- | ------------------------------------------------------------------------------ |
|
||||
| 생성결과 컨트롤러 | `backend/.../pwrkcnsltndoccrtrslt/PwrkCnsltnDocCrtRsltController.java` |
|
||||
| 실무협의관리 컨트롤러 | `backend/.../pwrkcnsltnmng/PwrkCnsltnMngController.java` |
|
||||
| DC RAG TO 컨트롤러 | `backend/.../dcrag/DcRagApiToController.java` |
|
||||
| 초안생성 데이터 설명 | `02.03.초안생성_데이터_설명.md` |
|
||||
| 엔티티 구현 스킬 | `frontend/.cursor/skills/backend-controller-to-entity/` |
|
||||
| 재원협의 엔티티 (패턴) | `apps/as/src/entities/fnrs-cnsltn/` |
|
||||
| IIPBS 단어목록 | `frontend/docs/IIPBS_단어목록.md` |
|
||||
| 진행상태 코드 | `backend/IIPBS_CS/docs/2026/05.행정지원/01.문서상태별_사용자_상태변경_이력.md` |
|
||||
@@ -33,10 +33,10 @@
|
||||
├──────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 B] 필수첨부파일 목록 │
|
||||
│ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 필수첨부파일아이디 | 문서구분 | 필수첨부파일명 | 필수첨부여부 | 비고 | 등록일시 | 수정일시 | 관리 │
|
||||
│ │ 필수첨부파일아이디 | 문서구분 | 필수첨부파일명 | 필수첨부여부 | 필수파일검토내용 | 비고 | 관리 │ │
|
||||
│ ├────────────────────────────────────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ EAF-0001 | 설치인가 | 사업계획서 | 예 | 서명본 첨부 | 2026-04-22 10:10:00 | — | [수정][삭제] │ │
|
||||
│ │ EAF-0002 | 기본계획 | 변경사유서 | 예 | — | 2026-04-22 11:00:00 | — | [수정][삭제] │ │
|
||||
│ │ EAF-0001 | 설치인가 | 사업계획서 | 예 | 사업비 내역 검토 | 서명본 첨부 | [수정][삭제] │ │
|
||||
│ │ EAF-0002 | 기본계획 | 변경사유서 | 예 | 변경 전후 비교표 | — | [수정][삭제] │ │
|
||||
│ └────────────────────────────────────────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
│ 필수첨부파일아이디(자동발급) [EAF-0004] │
|
||||
│ 문서구분 [설치인가 ▼] │
|
||||
│ 필수첨부파일명 [예: 사업계획서] │
|
||||
│ 양식파일 [파일 선택] │
|
||||
│ 필수첨부여부 [ ] 예 [ ] 아니오 │
|
||||
│ 필수파일검토내용 [검토내용을 입력하세요]│
|
||||
│ 비고 [비고를 입력하세요] │
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ [취소] [저장] │
|
||||
@@ -139,16 +141,16 @@ END
|
||||
|
||||
### 5-2. 그리드 컬럼
|
||||
|
||||
| 컬럼명 | Field | 타입 | 비고 |
|
||||
| ------------------ | ------------------- | ------ | ------------------------------------- |
|
||||
| 필수첨부파일아이디 | `esntlAtchFileId` | string | 예: `EAF-0001`, 클라이언트 생성/입력 |
|
||||
| 문서구분 | `docSeComCd` | string | 코드명 표시 — 공통코드 (TBD 상위코드) |
|
||||
| 필수첨부파일명 | `esntlAtchFileNm` | string | |
|
||||
| 필수첨부여부 | `esntlAtchYn` | string | `Y`→예, `N`→아니오 |
|
||||
| 비고 | `esntlAtchFileRmrk` | string | 없으면 `—` 표시 |
|
||||
| 등록일시 | `regDt` | string | `yyyy-MM-dd HH:mm:ss` |
|
||||
| 수정일시 | `mdfcnDt` | string | 미수정 시 빈 값 또는 `—` |
|
||||
| 관리 | — | — | 행별 **[수정] · [삭제]** |
|
||||
| 컬럼명 | Field | 타입 | 비고 |
|
||||
| ------------------ | -------------------- | ------ | ------------------------------------- |
|
||||
| 필수첨부파일아이디 | `esntlAtchFileId` | string | 예: `EAF-0001`, 클라이언트 생성/입력 |
|
||||
| 문서구분 | `docSeComCd` | string | 코드명 표시 — 공통코드 (TBD 상위코드) |
|
||||
| 필수첨부파일명 | `esntlAtchFileNm` | string | |
|
||||
| 필수첨부여부 | `esntlAtchYn` | string | `Y`→예, `N`→아니오 |
|
||||
| 필수파일검토내용 | `esntlAtchFileRvwCn` | string | 없으면 `—` 표시, 최대 2줄 clamp |
|
||||
| 비고 | `esntlAtchFileRmrk` | string | 없으면 `—` 표시 |
|
||||
| 양식 내려받기 | `atchFileGroupId` | string | 양식파일 다운로드 버튼 |
|
||||
| 관리 | — | — | 행별 **[수정] · [삭제]** |
|
||||
|
||||
- 행 클릭 시 별도 상세 화면 없음. 편집은 **[수정]** 버튼으로 다이얼로그 C를 연다.
|
||||
|
||||
@@ -167,13 +169,15 @@ END
|
||||
|
||||
### 6-1. 폼 필드
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| ------------------ | ---------- | ---- | ------ | ------------------- | -------------------------------- |
|
||||
| 필수첨부파일아이디 | (Readonly) | Y | — | `esntlAtchFileId` | 등록 시 미노출 |
|
||||
| 문서구분 | CodeSelect | Y | — | `docSeComCd` | 예: 설치인가, 기본계획 |
|
||||
| 필수첨부파일명 | Text | Y | — | `esntlAtchFileNm` | placeholder: "예: 사업계획서" |
|
||||
| 필수첨부여부 | RadioGroup | Y | Y | `esntlAtchYn` | Y/N, UI 표시: 예/아니오 |
|
||||
| 비고 | Textarea | N | — | `esntlAtchFileRmrk` | placeholder: "비고를 입력하세요" |
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| ------------------ | ------------------- | ---- | ------ | -------------------- | ---------------------------------- |
|
||||
| 필수첨부파일아이디 | (Readonly) | Y | — | `esntlAtchFileId` | 등록 시 미노출 |
|
||||
| 문서구분 | CodeSelect | Y | — | `docSeComCd` | 예: 설치인가, 기본계획 |
|
||||
| 필수첨부파일명 | Text | Y | — | `esntlAtchFileNm` | placeholder: "예: 사업계획서" |
|
||||
| 양식파일 | AttachmentFileField | N | — | `atchFileGroupId` | TUS 파일 업로드 |
|
||||
| 필수첨부여부 | RadioGroup | Y | Y | `esntlAtchYn` | Y/N, UI 표시: 예/아니오 |
|
||||
| 필수파일검토내용 | Textarea | N | — | `esntlAtchFileRvwCn` | placeholder: "검토내용을 입력하세요"|
|
||||
| 비고 | Textarea | N | — | `esntlAtchFileRmrk` | placeholder: "비고를 입력하세요" |
|
||||
|
||||
### 6-2. 버튼
|
||||
|
||||
@@ -210,28 +214,32 @@ END
|
||||
|
||||
**응답 DTO**
|
||||
|
||||
| Field | 타입 | 설명 |
|
||||
| ------------------- | ------- | -------------------------------- |
|
||||
| `esntlAtchFileId` | string | 필수첨부파일아이디 |
|
||||
| `docSeComCd` | string | 문서구분공통코드 |
|
||||
| `esntlAtchFileNm` | string | 필수첨부파일명 |
|
||||
| `esntlAtchYn` | string | 필수첨부여부 (Y/N) |
|
||||
| `esntlAtchFileRmrk` | string? | 필수첨부파일비고 |
|
||||
| `rgtr` | string? | 등록자 UUID |
|
||||
| `rgtrNm` | string? | 등록자명 |
|
||||
| `mdfr` | string? | 수정자 UUID |
|
||||
| `regDt` | string? | 등록일시 (`yyyy-MM-dd HH:mm:ss`) |
|
||||
| `mdfcnDt` | string? | 수정일시 |
|
||||
| Field | 타입 | 설명 |
|
||||
| -------------------- | -------------------------- | -------------------------------- |
|
||||
| `esntlAtchFileId` | string | 필수첨부파일아이디 |
|
||||
| `docSeComCd` | string | 문서구분공통코드 |
|
||||
| `esntlAtchFileNm` | string | 필수첨부파일명 |
|
||||
| `esntlAtchYn` | string | 필수첨부여부 (Y/N) |
|
||||
| `esntlAtchFileRvwCn` | string? | 필수첨부파일검토내용 |
|
||||
| `esntlAtchFileRmrk` | string? | 필수첨부파일비고 |
|
||||
| `atchFileGroupId` | string? | 첨부파일그룹아이디 (양식파일) |
|
||||
| `atchFileList` | AtflMngClientResponseDto[]?| 첨부파일 목록 |
|
||||
| `rgtr` | string? | 등록자 UUID |
|
||||
| `rgtrNm` | string? | 등록자명 |
|
||||
| `mdfr` | string? | 수정자 UUID |
|
||||
| `regDt` | string? | 등록일시 (`yyyy-MM-dd HH:mm:ss`) |
|
||||
| `mdfcnDt` | string? | 수정일시 |
|
||||
|
||||
**저장 Request**
|
||||
|
||||
| Field | 필수 | 설명 |
|
||||
| ------------------- | ---- | ------------------ |
|
||||
| `esntlAtchFileId` | Y | 필수첨부파일아이디 |
|
||||
| `docSeComCd` | N | 문서구분 |
|
||||
| `esntlAtchFileNm` | N | 필수첨부파일명 |
|
||||
| `esntlAtchYn` | N | 필수첨부여부 |
|
||||
| `esntlAtchFileRmrk` | N | 비고 |
|
||||
| Field | 필수 | 설명 |
|
||||
| -------------------- | ---- | -------------------- |
|
||||
| `docSeComCd` | Y | 문서구분 |
|
||||
| `esntlAtchFileNm` | N | 필수첨부파일명 |
|
||||
| `esntlAtchYn` | N | 필수첨부여부 |
|
||||
| `esntlAtchFileRvwCn` | N | 필수첨부파일검토내용 |
|
||||
| `esntlAtchFileRmrk` | N | 비고 |
|
||||
| `atchFileGroupId` | N | 첨부파일그룹아이디 |
|
||||
|
||||
**검색 Query**
|
||||
|
||||
|
||||
@@ -2,6 +2,12 @@ import type { NextConfig } from "next";
|
||||
|
||||
const subDomain = process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000";
|
||||
|
||||
const webhwpCtrlBase = (
|
||||
process.env.NEXT_PUBLIC_WEBHYP_CTRL_URL || "https://hwp.onasys.co.kr/webhwpctrl"
|
||||
).replace(/\/$/, "");
|
||||
|
||||
const webhwpAssetPrefixes = ["js", "hwpsdk", "dist", "commonFrame", "wasm"] as const;
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
reactStrictMode: true,
|
||||
@@ -30,6 +36,14 @@ const nextConfig: NextConfig = {
|
||||
source: "/cms/:path*",
|
||||
destination: `${subDomain}/cms/:path*`,
|
||||
},
|
||||
...webhwpAssetPrefixes.map((prefix) => ({
|
||||
source: `/webhwpctrl/${prefix}/:path*`,
|
||||
destination: `${webhwpCtrlBase}/${prefix}/:path*`,
|
||||
})),
|
||||
{
|
||||
source: "/webhwpctrl/file/:path*",
|
||||
destination: `${webhwpCtrlBase}/file/:path*`,
|
||||
},
|
||||
],
|
||||
fallback: [],
|
||||
};
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3003",
|
||||
"dev": "next dev -p 3004",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3003",
|
||||
"start": "next start -p 3004",
|
||||
"lint": "next lint",
|
||||
"lint:fsd": "steiger ."
|
||||
},
|
||||
@@ -36,6 +36,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@hookform/devtools": "^4.4.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "15.3.8",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "as",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3004",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3004",
|
||||
"lint": "next lint",
|
||||
"lint:fsd": "steiger .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@common/assets": "workspace:*",
|
||||
"@common/auth": "workspace:*",
|
||||
"@common/components": "workspace:*",
|
||||
"@common/network": "workspace:*",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@faker-js/faker": "^9.9.0",
|
||||
"@frontend-opensource/use-react-hooks": "^1.1.4",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tanstack/react-query": "^5.75.5",
|
||||
"@tanstack/react-query-devtools": "^5.75.5",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"axios": "^1.8.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.474.0",
|
||||
"next": "15.5.18",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"tus-js-client": "^4.3.1",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@hookform/devtools": "^4.4.0",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@testing-library/jest-dom": "^7.0.1",
|
||||
"@testing-library/react": "^16.3.3",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitejs/plugin-react-oxc": "^0.4.3",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"jsdom": "^30.1.2",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"steiger": "^0.5.11",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^8.3.3",
|
||||
"vitest": "^5.0.3"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 483 KiB |
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*/
|
||||
/**
|
||||
* Jquery integration plugin.
|
||||
*
|
||||
* @class tinymce.core.JqueryIntegration
|
||||
* @private
|
||||
*/
|
||||
!function(){function f(){
|
||||
// Reference to tinymce needs to be lazily evaluated since tinymce
|
||||
// might be loaded through the compressor or other means
|
||||
return d.tinymce}var p,c,u,s=[],d="undefined"!=typeof global?global:window,m=d.jQuery;m.fn.tinymce=function(o){var e,t,i,n,l=this,r="";
|
||||
// No match then just ignore the call
|
||||
return l.length?
|
||||
// Get editor instance
|
||||
o?(l.css("visibility","hidden"),
|
||||
// Load TinyMCE on demand, if we need to
|
||||
d.tinymce||c||!(e=o.script_url)?
|
||||
// Delay the init call until tinymce is loaded
|
||||
1===c?s.push(a):a():(c=1,t=e.substring(0,e.lastIndexOf("/")),
|
||||
// Check if it's a dev/src version they want to load then
|
||||
// make sure that all plugins, themes etc are loaded in source mode as well
|
||||
-1!=e.indexOf(".min")&&(r=".min"),
|
||||
// Setup tinyMCEPreInit object this will later be used by the TinyMCE
|
||||
// core script to locate other resources like CSS files, dialogs etc
|
||||
// You can also predefined a tinyMCEPreInit object and then it will use that instead
|
||||
d.tinymce=d.tinyMCEPreInit||{base:t,suffix:r},
|
||||
// url contains gzip then we assume it's a compressor
|
||||
-1!=e.indexOf("gzip")&&(i=o.language||"en",e=e+(/\?/.test(e)?"&":"?")+"js=true&core=true&suffix="+escape(r)+"&themes="+escape(o.theme||"modern")+"&plugins="+escape(o.plugins||"")+"&languages="+(i||""),
|
||||
// Check if compressor script is already loaded otherwise setup a basic one
|
||||
d.tinyMCE_GZ||(d.tinyMCE_GZ={start:function(){function n(e){f().ScriptLoader.markDone(f().baseURI.toAbsolute(e))}
|
||||
// Add core languages
|
||||
n("langs/"+i+".js"),
|
||||
// Add themes with languages
|
||||
n("themes/"+o.theme+"/theme"+r+".js"),n("themes/"+o.theme+"/langs/"+i+".js"),
|
||||
// Add plugins with languages
|
||||
m.each(o.plugins.split(","),function(e,t){t&&(n("plugins/"+t+"/plugin"+r+".js"),n("plugins/"+t+"/langs/"+i+".js"))})},end:function(){}})),(n=document.createElement("script")).type="text/javascript",n.onload=n.onreadystatechange=function(e){e=e||window.event,2===c||"load"!=e.type&&!/complete|loaded/.test(n.readyState)||(f().dom.Event.domLoaded=1,c=2,
|
||||
// Execute callback after mainscript has been loaded and before the initialization occurs
|
||||
o.script_loaded&&o.script_loaded(),a(),m.each(s,function(e,t){t()}))},n.src=e,document.body.appendChild(n)),l):f()?f().get(l[0].id):null:l;function a(){var a=[],c=0;
|
||||
// Apply patches to the jQuery object, only once
|
||||
u||(v(),u=!0),
|
||||
// Create an editor instance for each matched node
|
||||
l.each(function(e,t){var n,i=t.id,r=o.oninit;
|
||||
// Generate unique id for target element if needed
|
||||
i||(t.id=i=f().DOM.uniqueId()),
|
||||
// Only init the editor once
|
||||
f().get(i)||(
|
||||
// Create editor instance and render it
|
||||
n=f().createEditor(i,o),a.push(n),n.on("init",function(){var e,t=r;l.css("visibility",""),
|
||||
// Run this if the oninit setting is defined
|
||||
// this logic will fire the oninit callback ones each
|
||||
// matched editor instance is initialized
|
||||
r&&++c==a.length&&("string"==typeof t&&(e=-1===t.indexOf(".")?null:f().resolve(t.replace(/\.\w+$/,"")),t=f().resolve(t)),
|
||||
// Call the oninit function with the object
|
||||
t.apply(e||f(),a))}))}),
|
||||
// Render the editor instances in a separate loop since we
|
||||
// need to have the full editors array used in the onInit calls
|
||||
m.each(a,function(e,t){t.render()})}},
|
||||
// Add :tinymce pseudo selector this will select elements that has been converted into editor instances
|
||||
// it's now possible to use things like $('*:tinymce') to get all TinyMCE bound elements.
|
||||
m.extend(m.expr[":"],{tinymce:function(e){var t;return!!(e.id&&"tinymce"in d&&(t=f().get(e.id))&&t.editorManager===f())}});
|
||||
// This function patches internal jQuery functions so that if
|
||||
// you for example remove an div element containing an editor it's
|
||||
// automatically destroyed by the TinyMCE API
|
||||
var v=function(){function r(e){
|
||||
// If the function is remove
|
||||
"remove"===e&&this.each(function(e,t){var n=u(t);n&&n.remove()}),this.find("span.mceEditor,div.mceEditor").each(function(e,t){var n=f().get(t.id.replace(/_parent$/,""));n&&n.remove()})}function o(i){var e,t=this;
|
||||
// Handle set value
|
||||
/*jshint eqnull:true */if(null!=i)r.call(t),
|
||||
// Saves the contents before get/set value of textarea/div
|
||||
t.each(function(e,t){var n;(n=f().get(t.id))&&n.setContent(i)});else if(0<t.length&&(e=f().get(t[0].id)))return e.getContent()}function l(e){return e&&e.length&&d.tinymce&&e.is(":tinymce")}
|
||||
// Removes any child editor instances by looking for editor wrapper elements
|
||||
var u=function(e){var t=null;return e&&e.id&&d.tinymce?f().get(e.id):t},s={};
|
||||
// Loads or saves contents from/to textarea if the value
|
||||
// argument is defined it will set the TinyMCE internal contents
|
||||
// Patch some setter/getter functions these will
|
||||
// now be able to set/get the contents of editor instances for
|
||||
// example $('#editorid').html('Content'); will update the TinyMCE iframe instance
|
||||
m.each(["text","html","val"],function(e,t){var a=s[t]=m.fn[t],c="text"===t;m.fn[t]=function(e){var t=this;if(!l(t))return a.apply(t,arguments);if(e!==p)return o.call(t.filter(":tinymce"),e),a.apply(t.not(":tinymce"),arguments),t;// return original set for chaining
|
||||
var i="",r=arguments;return(c?t:t.eq(0)).each(function(e,t){var n=u(t);i+=n?c?n.getContent().replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g,""):n.getContent({save:!0}):a.apply(m(t),r)}),i}}),
|
||||
// Makes it possible to use $('#id').append("content"); to append contents to the TinyMCE editor iframe
|
||||
m.each(["append","prepend"],function(e,t){var n=s[t]=m.fn[t],r="prepend"===t;m.fn[t]=function(i){var e=this;return l(e)?i!==p?("string"==typeof i&&e.filter(":tinymce").each(function(e,t){var n=u(t);n&&n.setContent(r?i+n.getContent():n.getContent()+i)}),n.apply(e.not(":tinymce"),arguments),e):void 0:n.apply(e,arguments)}}),
|
||||
// Makes sure that the editor instance gets properly destroyed when the parent element is removed
|
||||
m.each(["remove","replaceWith","replaceAll","empty"],function(e,t){var n=s[t]=m.fn[t];m.fn[t]=function(){return r.call(this,t),n.apply(this,arguments)}}),s.attr=m.fn.attr,
|
||||
// Makes sure that $('#tinymce_id').attr('value') gets the editors current HTML contents
|
||||
m.fn.attr=function(e,t){var n=this,i=arguments;if(!e||"value"!==e||!l(n))return s.attr.apply(n,i);if(t!==p)return o.call(n.filter(":tinymce"),t),s.attr.apply(n.not(":tinymce"),i),n;// return original set for chaining
|
||||
var r=n[0],a=u(r);return a?a.getContent({save:!0}):s.attr.apply(m(r),i)}}}();
|
||||
@@ -0,0 +1,3 @@
|
||||
This is where language files should be placed.
|
||||
|
||||
Please DO NOT translate these directly use this service: https://www.transifex.com/projects/p/tinymce/
|
||||
@@ -0,0 +1,504 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
not price. Our General Public Licenses are designed to make sure that
|
||||
you have the freedom to distribute copies of free software (and charge
|
||||
for this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a
|
||||
table of data to be supplied by an application program that uses
|
||||
the facility, other than as an argument passed when the facility
|
||||
is invoked, then you must make a good faith effort to ensure that,
|
||||
in the event an application does not supply such function or
|
||||
table, the facility still operates, and performs whatever part of
|
||||
its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||
License instead of this License to a given copy of the Library. To do
|
||||
this, you must alter all the notices that refer to this License, so
|
||||
that they refer to the ordinary GNU General Public License, version 2,
|
||||
instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of
|
||||
the Library into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the
|
||||
source code from the same place satisfies the requirement to
|
||||
distribute the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a
|
||||
work, in isolation, is not a derivative work of the Library, and
|
||||
therefore falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License.
|
||||
Section 6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data
|
||||
structure layouts and accessors, and small macros and small inline
|
||||
functions (ten lines or less in length), then the use of the object
|
||||
file is unrestricted, regardless of whether it is legally a derivative
|
||||
work. (Executables containing this object code plus portions of the
|
||||
Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
modification of the work for the customer's own use and reverse
|
||||
engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood
|
||||
that the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (1) uses at run time a
|
||||
copy of the library already present on the user's computer system,
|
||||
rather than copying library functions into the executable, and (2)
|
||||
will operate properly with a modified version of the library, if
|
||||
the user installs one, as long as the modified version is
|
||||
interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities. This must be distributed under the terms of the
|
||||
Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the
|
||||
Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Library at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Library by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License may add
|
||||
an explicit geographical distribution limitation excluding those countries,
|
||||
so that distribution is permitted only in or among countries not thus
|
||||
excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Lesser General Public License from time to time.
|
||||
Such new versions will be similar in spirit to the present version,
|
||||
but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest
|
||||
possible use to the public, we recommend making it free software that
|
||||
everyone can redistribute and change. You can do so by permitting
|
||||
redistribution under these terms (or, alternatively, under the terms of the
|
||||
ordinary General Public License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It is
|
||||
safest to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least the
|
||||
"copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the library's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1990
|
||||
Ty Coon, President of Vice
|
||||
|
||||
That's all there is to it!
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function f(t,e,n){t.execCommand("UL"===e?"InsertUnorderedList":"InsertOrderedList",!1,!1===n?null:{"list-style-type":n})}function i(t){return function(){return t}}function t(t){return t}function e(){return s}var n=tinymce.util.Tools.resolve("tinymce.PluginManager"),g=tinymce.util.Tools.resolve("tinymce.util.Tools"),u=i(!1),l=i(!0),s={fold:function(t,e){return t()},isSome:u,isNone:l,getOr:t,getOrThunk:r,getOrDie:function(t){throw new Error(t||"error: getOrDie called on none.")},getOrNull:i(null),getOrUndefined:i(void 0),or:t,orThunk:r,map:e,each:function(){},bind:e,exists:u,forall:l,filter:function(){return s},toArray:function(){return[]},toString:i("none()")};function r(t){return t()}function d(t){return t&&/^(TH|TD)$/.test(t.nodeName)}function m(c,a){return function(s){function t(t){var e,n,r,o,i,u,l;s.setActive((e=c,r=a,i=-1!==(o=function(t,e){for(var n=0;n<t.length;n++)if(e(t[n]))return n;return-1}((n=t).parents,d))?n.parents.slice(0,o):n.parents,0<(u=g.grep(i,(l=e,function(t){return t&&/^(OL|UL|DL)$/.test(t.nodeName)&&(e=t,l.$.contains(l.getBody(),e));var e}))).length&&u[0].nodeName===r))}return c.on("NodeChange",t),function(){return c.off("NodeChange",t)}}}function c(t,e,n,r,o,i){var u,l,s,c,a,d;1<i.length?(c=r,a=o,d=i,(s=t).ui.registry.addSplitButton(e,{tooltip:n,icon:"OL"===a?"ordered-list":"unordered-list",presets:"listpreview",columns:3,fetch:function(t){t(g.map(d,function(t){return{type:"choiceitem",value:"default"===t?"":t,icon:"list-"+("OL"===a?"num":"bull")+"-"+("disc"===t||"decimal"===t?"default":t),text:t.replace(/\-/g," ").replace(/\b\w/g,function(t){return t.toUpperCase()})}}))},onAction:function(){return s.execCommand(c)},onItemAction:function(t,e){f(s,a,e)},select:function(e){var t,n=(t=s).dom.getParent(t.selection.getNode(),"ol,ul"),r=t.dom.getStyle(n,"listStyleType");return p(r).map(function(t){return e===t}).getOr(!1)},onSetup:m(s,a)})):(l=r,(u=t).ui.registry.addToggleButton(e,{active:!1,tooltip:n,icon:"OL"===o?"ordered-list":"unordered-list",onSetup:m(u,o),onAction:function(){return u.execCommand(l)}}))}var a=function(n){function t(){return o}function e(t){return t(n)}var r=i(n),o={fold:function(t,e){return e(n)},isSome:l,isNone:u,getOr:r,getOrThunk:r,getOrDie:r,getOrNull:r,getOrUndefined:r,or:t,orThunk:t,map:function(t){return a(t(n))},each:function(t){t(n)},bind:e,exists:e,forall:e,filter:function(t){return t(n)?o:s},toArray:function(){return[n]},toString:function(){return"some("+n+")"}};return o},p=function(t){return null==t?s:a(t)};n.add("advlist",function(t){var n,e,r,o;t.hasPlugin("lists")?(c(e=t,"numlist","Numbered list","InsertOrderedList","OL",(r=e.getParam("advlist_number_styles","default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman"))?r.split(/[ ,]/):[]),c(e,"bullist","Bullet list","InsertUnorderedList","UL",(o=e.getParam("advlist_bullet_styles","default,circle,square"))?o.split(/[ ,]/):[]),(n=t).addCommand("ApplyUnorderedListStyle",function(t,e){f(n,"UL",e["list-style-type"])}),n.addCommand("ApplyOrderedListStyle",function(t,e){f(n,"OL",e["list-style-type"])})):console.error("Please use the Lists plugin together with the Advanced List plugin.")})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function a(e){return e.getAttribute("id")||e.getAttribute("name")||""}function c(e){return e&&"a"===e.nodeName.toLowerCase()&&!e.getAttribute("href")&&""!==a(e)}function d(e){return e.dom.getParent(e.selection.getStart(),l)}function r(e,t){var o,a,n,r,i,l=d(e);l?(n=e,r=t,(i=l).removeAttribute("name"),i.id=r,n.addVisual(),n.undoManager.add()):(a=t,(o=e).undoManager.transact(function(){var e,n;o.getParam("allow_html_in_named_anchor",!1,"boolean")||o.selection.collapse(!0),o.selection.isCollapsed()?o.insertContent(o.dom.createHTML("a",{id:a})):(n=(e=o).dom,u(n).walk(e.selection.getRng(),function(e){s.each(e,function(e){var t;c(t=e)&&!t.firstChild&&n.remove(e,!1)})}),o.formatter.remove("namedAnchor",null,null,!0),o.formatter.apply("namedAnchor",{value:a}),o.addVisual())})),e.focus()}function i(r){return function(e){for(var t,n=0;n<e.length;n++){var o=e[n],a=void 0;!(a=t=o)||a.attr("href")||!a.attr("id")&&!a.attr("name")||t.firstChild||o.attr("contenteditable",r)}}}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),u=tinymce.util.Tools.resolve("tinymce.dom.RangeUtils"),s=tinymce.util.Tools.resolve("tinymce.util.Tools"),l="a:not([href])";e.add("anchor",function(e){var t,n,o;(t=e).on("PreInit",function(){t.parser.addNodeFilter("a",i("false")),t.serializer.addNodeFilter("a",i(null))}),(n=e).addCommand("mceAnchor",function(){var o,e,t;t=(e=d(o=n))?a(e):"",o.windowManager.open({title:"Anchor",size:"normal",body:{type:"panel",items:[{name:"id",type:"input",label:"ID",placeholder:"example"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{id:t},onSubmit:function(e){var t=o,n=e.getData().id;(/^[A-Za-z][A-Za-z0-9\-:._]*$/.test(n)?(r(t,n),0):(t.windowManager.alert("Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores."),1))||e.close()}})}),(o=e).ui.registry.addToggleButton("anchor",{icon:"bookmark",tooltip:"Anchor",onAction:function(){return o.execCommand("mceAnchor")},onSetup:function(e){return o.selection.selectorChangedWithUnbind("a:not([href])",e.setActive).unbind}}),o.ui.registry.addMenuItem("anchor",{icon:"bookmark",text:"Anchor...",onAction:function(){return o.execCommand("mceAnchor")}}),e.on("PreInit",function(){e.formatter.register("namedAnchor",{inline:"a",selector:l,remove:"all",split:!0,deep:!0,attributes:{id:"%value"},onmatch:c})})})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function k(e){return/^[(\[{ \u00a0]$/.test(e)}function w(e){return 3===e.nodeType}function i(e){return 1===e.nodeType}function o(e,t){var n;return t<0&&(t=0),!w(e)||(n=e.data.length)<t&&(t=n),t}function y(e,t,n){!i(t)||t.hasChildNodes()?e.setStart(t,o(t,n)):e.setStartBefore(t)}function v(e,t,n){!i(t)||t.hasChildNodes()?e.setEnd(t,o(t,n)):e.setEndAfter(t)}function r(e,t){var n,i,o,r,a,f=e.getParam("autolink_pattern",A),s=e.getParam("default_link_target",!1);if(null===e.dom.getParent(e.selection.getNode(),"a[href]")){var d=e.selection.getRng().cloneRange();if(d.startOffset<5){if(!(r=d.endContainer.previousSibling)){if(!d.endContainer.firstChild||!d.endContainer.firstChild.nextSibling)return;r=d.endContainer.firstChild.nextSibling}if(y(d,r,a=r.length),v(d,r,a),d.endOffset<5)return;n=d.endOffset,i=r}else{if(!w(i=d.endContainer)&&i.firstChild){for(;!w(i)&&i.firstChild;)i=i.firstChild;w(i)&&(y(d,i,0),v(d,i,i.nodeValue.length))}n=1===d.endOffset?2:d.endOffset-1-t}for(var l=n;y(d,i,2<=n?n-2:0),v(d,i,1<=n?n-1:0),--n,!k(d.toString())&&0<=n-2;);k(d.toString())?(y(d,i,n),v(d,i,l),n+=1):(0===d.startOffset?y(d,i,0):y(d,i,n),v(d,i,l)),u=d.toString(),/[?!,.;:]/.test(u.charAt(u.length-1))&&v(d,i,l-1);var u,c,g,h,C=(u=d.toString().trim()).match(f),m=e.getParam("link_default_protocol","http","string");C&&((g=c=C[0]).length>=(h="www.").length&&g.substr(0,0+h.length)===h?c=m+"://"+c:-1===c.indexOf("@")||/^([A-Za-z][A-Za-z\d.+-]*:\/\/)|mailto:/.test(c)||(c="mailto:"+c),o=e.selection.getBookmark(),e.selection.setRng(d),e.execCommand("createlink",!1,c),!1!==s&&e.dom.setAttrib(e.selection.getNode(),"target",s),e.selection.moveToBookmark(o),e.nodeChanged())}}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=tinymce.util.Tools.resolve("tinymce.Env"),A=new RegExp("^"+/(?:[A-Za-z][A-Za-z\d.+-]{0,14}:\/\/(?:[-.~*+=!&;:'%@?^${}(),\w]+@)?|www\.|[-;:&=+$,.\w]+@)[A-Za-z\d-]+(?:\.[A-Za-z\d-]+)*(?::\d+)?(?:\/(?:[-.~*+=!;:'%@$(),\/\w]*[-~*+=%@$()\/\w])?)?(?:\?(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?(?:#(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?/g.source+"$","i");e.add("autolink",function(e){var t,n;(t=e).on("keydown",function(e){if(13===e.keyCode)return r(t,-1)}),a.browser.isIE()?t.on("focus",function(){if(!n){n=!0;try{t.execCommand("AutoUrlDetect",!1,!0)}catch(e){}}}):(t.on("keypress",function(e){if(41===e.keyCode||93===e.keyCode||125===e.keyCode)return r(t,-1)}),t.on("keyup",function(e){if(32===e.keyCode)return r(t,0)}))})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function y(e){return e.getParam("min_height",e.getElement().offsetHeight,"number")}function p(e,t){var n=e.getBody();n&&(n.style.overflowY=t?"":"hidden",t||(n.scrollTop=0))}function v(e,t,n,i){var o=parseInt(e.getStyle(t,n,i),10);return isNaN(o)?0:o}var l=Object.hasOwnProperty,e=tinymce.util.Tools.resolve("tinymce.PluginManager"),b=tinymce.util.Tools.resolve("tinymce.Env"),r=tinymce.util.Tools.resolve("tinymce.util.Delay"),u=function(e,t,n,i,o){r.setEditorTimeout(e,function(){C(e,t),n--?u(e,t,n,i,o):o&&o()},i)},C=function(e,t,n){var i,o,r,s,a,l,u,g,c,m,f,d=e.dom,h=e.getDoc();h&&(e.plugins.fullscreen&&e.plugins.fullscreen.isFullscreen()?p(e,!0):(i=h.documentElement,o=e.getParam("autoresize_bottom_margin",50,"number"),r=y(e),s=v(d,i,"margin-top",!0),a=v(d,i,"margin-bottom",!0),(l=(l=i.offsetHeight+s+a+o)<0?0:l)+(u=e.getContainer().offsetHeight-e.getContentAreaContainer().offsetHeight)>y(e)&&(r=l+u),(g=e.getParam("max_height",0,"number"))&&g<r?(r=g,p(e,!0)):p(e,!1),r!==t.get()&&(c=r-t.get(),d.setStyle(e.getContainer(),"height",r+"px"),t.set(r),e.fire("ResizeEditor"),b.browser.isSafari()&&b.mac&&(m=e.getWin()).scrollTo(m.pageXOffset,m.pageYOffset),!e.hasFocus()||"setcontent"!==(null==(f=n)?void 0:f.type.toLowerCase())||!0!==f.selection&&!0!==f.paste||e.selection.scrollIntoView(),b.webkit&&c<0&&C(e,t,n))))};e.add("autoresize",function(e){var t,n,i,o,r,s,a=e.settings;l.call(a,"resize")||(e.settings.resize=!1),e.inline||(s=0,r=t={get:function(){return s},set:function(e){s=e}},(o=e).addCommand("mceAutoResize",function(){C(o,r)}),i=t,(n=e).on("init",function(){var e=n.getParam("autoresize_overflow_padding",1,"number"),t=n.dom;t.setStyles(n.getDoc().documentElement,{height:"auto"}),t.setStyles(n.getBody(),{paddingLeft:e,paddingRight:e,"min-height":0})}),n.on("NodeChange SetContent keyup FullscreenStateChanged ResizeContent",function(e){C(n,i,e)}),n.getParam("autoresize_on_init",!0,"boolean")&&n.on("init",function(){u(n,i,20,100,function(){u(n,i,5,1e3)})}))})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function o(t,e){var r=t||e,n=/^(\d+)([ms]?)$/.exec(""+r);return(n[2]?{s:1e3,m:6e4}[n[2]]:1)*parseInt(r,10)}function n(t){var e=document.location;return t.getParam("autosave_prefix","tinymce-autosave-{path}{query}{hash}-{id}-").replace(/{path}/g,e.pathname).replace(/{query}/g,e.search).replace(/{hash}/g,e.hash).replace(/{id}/g,t.id)}function i(t,e){if(a(e))return t.dom.isEmpty(t.getBody());var r=d.trim(e);if(""===r)return!0;var n=(new DOMParser).parseFromString(r,"text/html");return t.dom.isEmpty(n)}function u(t){var e=parseInt(v.getItem(n(t)+"time"),10)||0;return!((new Date).getTime()-e>o(t.getParam("autosave_retention"),"20m")&&(g(t,!1),1))}function s(t){var e=n(t);!i(t)&&t.isDirty()&&(v.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),v.setItem(e+"time",(new Date).getTime().toString()),t.fire("StoreDraft"))}function f(t){var e=n(t);u(t)&&(t.setContent(v.getItem(e+"draft"),{format:"raw"}),t.fire("RestoreDraft"))}function c(t){t.undoManager.transact(function(){f(t),g(t)}),t.focus()}function m(r){return function(t){function e(){return t.setDisabled(!u(r))}return t.setDisabled(!u(r)),r.on("StoreDraft RestoreDraft RemoveDraft",e),function(){return r.off("StoreDraft RestoreDraft RemoveDraft",e)}}}var t=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=function(t){return void 0===t},l=tinymce.util.Tools.resolve("tinymce.util.Delay"),v=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),d=tinymce.util.Tools.resolve("tinymce.util.Tools"),g=function(t,e){var r=n(t);v.removeItem(r+"draft"),v.removeItem(r+"time"),!1!==e&&t.fire("RemoveDraft")},y=tinymce.util.Tools.resolve("tinymce.EditorManager");t.add("autosave",function(t){var e,r,n,a;return t.editorManager.on("BeforeUnload",function(t){var e;d.each(y.get(),function(t){t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&t.getParam("autosave_ask_before_unload",!0)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))}),e&&(t.preventDefault(),t.returnValue=e)}),n=e=t,a=o(n.getParam("autosave_interval"),"30s"),l.setEditorInterval(n,function(){s(n)},a),e.ui.registry.addButton("restoredraft",{tooltip:"Restore last draft",icon:"restore-draft",onAction:function(){c(e)},onSetup:m(e)}),e.ui.registry.addMenuItem("restoredraft",{text:"Restore last draft",icon:"restore-draft",onAction:function(){c(e)},onSetup:m(e)}),t.on("init",function(){t.getParam("autosave_restore_when_empty",!1)&&t.dom.isEmpty(t.getBody())&&f(t)}),r=t,{hasDraft:function(){return u(r)},storeDraft:function(){return s(r)},restoreDraft:function(){return f(r)},removeDraft:function(t){return g(r,t)},isEmpty:function(t){return i(r,t)}}})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function i(t){function o(o,e){t=t.replace(o,e)}return t=n.trim(t),o(/\n/gi,"<br />"),o(/\[b\]/gi,"<strong>"),o(/\[\/b\]/gi,"</strong>"),o(/\[i\]/gi,"<em>"),o(/\[\/i\]/gi,"</em>"),o(/\[u\]/gi,"<u>"),o(/\[\/u\]/gi,"</u>"),o(/\[url=([^\]]+)\](.*?)\[\/url\]/gi,'<a href="$1">$2</a>'),o(/\[url\](.*?)\[\/url\]/gi,'<a href="$1">$1</a>'),o(/\[img\](.*?)\[\/img\]/gi,'<img src="$1" />'),o(/\[color=(.*?)\](.*?)\[\/color\]/gi,'<font color="$1">$2</font>'),o(/\[code\](.*?)\[\/code\]/gi,'<span class="codeStyle">$1</span> '),o(/\[quote.*?\](.*?)\[\/quote\]/gi,'<span class="quoteStyle">$1</span> '),t}var o=tinymce.util.Tools.resolve("tinymce.PluginManager"),n=tinymce.util.Tools.resolve("tinymce.util.Tools");o.add("bbcode",function(o){o.on("BeforeSetContent",function(o){o.content=i(o.content)}),o.on("PostProcess",function(o){function e(o,e){t=t.replace(o,e)}var t;o.set&&(o.content=i(o.content)),o.get&&(o.content=(t=o.content,t=n.trim(t),e(/<a.*?href=\"(.*?)\".*?>(.*?)<\/a>/gi,"[url=$1]$2[/url]"),e(/<font.*?color=\"(.*?)\".*?class=\"codeStyle\".*?>(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),e(/<font.*?color=\"(.*?)\".*?class=\"quoteStyle\".*?>(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),e(/<font.*?class=\"codeStyle\".*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),e(/<font.*?class=\"quoteStyle\".*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),e(/<span style=\"color: ?(.*?);\">(.*?)<\/span>/gi,"[color=$1]$2[/color]"),e(/<font.*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[color=$1]$2[/color]"),e(/<span style=\"font-size:(.*?);\">(.*?)<\/span>/gi,"[size=$1]$2[/size]"),e(/<font>(.*?)<\/font>/gi,"$1"),e(/<img.*?src=\"(.*?)\".*?\/>/gi,"[img]$1[/img]"),e(/<span class=\"codeStyle\">(.*?)<\/span>/gi,"[code]$1[/code]"),e(/<span class=\"quoteStyle\">(.*?)<\/span>/gi,"[quote]$1[/quote]"),e(/<strong class=\"codeStyle\">(.*?)<\/strong>/gi,"[code][b]$1[/b][/code]"),e(/<strong class=\"quoteStyle\">(.*?)<\/strong>/gi,"[quote][b]$1[/b][/quote]"),e(/<em class=\"codeStyle\">(.*?)<\/em>/gi,"[code][i]$1[/i][/code]"),e(/<em class=\"quoteStyle\">(.*?)<\/em>/gi,"[quote][i]$1[/i][/quote]"),e(/<u class=\"codeStyle\">(.*?)<\/u>/gi,"[code][u]$1[/u][/code]"),e(/<u class=\"quoteStyle\">(.*?)<\/u>/gi,"[quote][u]$1[/u][/quote]"),e(/<\/(strong|b)>/gi,"[/b]"),e(/<(strong|b)>/gi,"[b]"),e(/<\/(em|i)>/gi,"[/i]"),e(/<(em|i)>/gi,"[i]"),e(/<\/u>/gi,"[/u]"),e(/<span style=\"text-decoration: ?underline;\">(.*?)<\/span>/gi,"[u]$1[/u]"),e(/<u>/gi,"[u]"),e(/<blockquote[^>]*>/gi,"[quote]"),e(/<\/blockquote>/gi,"[/quote]"),e(/<br \/>/gi,"\n"),e(/<br\/>/gi,"\n"),e(/<br>/gi,"\n"),e(/<p>/gi,""),e(/<\/p>/gi,"\n"),e(/ |\u00a0/gi," "),e(/"/gi,'"'),e(/</gi,"<"),e(/>/gi,">"),e(/&/gi,"&"),t))})})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("code",function(e){var t,o;function n(){return o.execCommand("mceCodeEditor")}return(t=e).addCommand("mceCodeEditor",function(){var n,e;e=(n=t).getContent({source_view:!0}),n.windowManager.open({title:"Source Code",size:"large",body:{type:"panel",items:[{type:"textarea",name:"code"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{code:e},onSubmit:function(e){var t=n,o=e.getData().code;t.focus(),t.undoManager.transact(function(){t.setContent(o)}),t.selection.setCursorLocation(),t.nodeChanged(),e.close()}})}),(o=e).ui.registry.addButton("code",{icon:"sourcecode",tooltip:"Source code",onAction:n}),o.ui.registry.addMenuItem("code",{icon:"sourcecode",text:"Source code",onAction:n}),{}})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("colorpicker",function(){})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("contextmenu",function(){})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("hr",function(n){var o,t;function e(){return t.execCommand("InsertHorizontalRule")}(o=n).addCommand("InsertHorizontalRule",function(){o.execCommand("mceInsertContent",!1,"<hr />")}),(t=n).ui.registry.addButton("hr",{icon:"horizontal-rule",tooltip:"Horizontal line",onAction:e}),t.ui.registry.addMenuItem("hr",{icon:"horizontal-rule",text:"Horizontal line",onAction:e})})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function t(n){return function(t){return r=typeof(e=t),(null===e?"null":"object"==r&&(Array.prototype.isPrototypeOf(e)||e.constructor&&"Array"===e.constructor.name)?"array":"object"==r&&(String.prototype.isPrototypeOf(e)||e.constructor&&"String"===e.constructor.name)?"string":r)===n;var e,r}}function y(t){return t.getParam("importcss_selector_converter")}function u(e){return l(e)?function(t){return-1!==t.indexOf(e)}:e instanceof RegExp?function(t){return e.test(t)}:e}function h(t,e){var r,n=/^(?:([a-z0-9\-_]+))?(\.[a-z0-9_\-\.]+)$/i.exec(e);if(n){var o=n[1],i=n[2].substr(1).split(".").join(" "),c=x.makeMap("a,img");return n[1]?(r={title:e},t.schema.getTextBlockElements()[o]?r.block=o:t.schema.getBlockElements()[o]||c[o.toLowerCase()]?r.selector=o:r.inline=o):n[2]&&(r={inline:"span",title:e.substr(1),classes:i}),!1!==t.getParam("importcss_merge_classes")?r.classes=i:r.attributes={class:i},r}}function d(t,e){return null===e||!1!==t.getParam("importcss_exclusive")}function r(v){v.on("init",function(){function o(t,e){if(f=t,p=g,!(d(v,m=e)?f in p:f in m.selectors)){a=t,l=g,d(v,u=e)?l[a]=!0:u.selectors[a]=!0;var r=(i=(o=v).plugins.importcss,c=t,((s=e)&&s.selector_converter?s.selector_converter:y(o)?y(o):function(){return h(o,c)}).call(i,c,s));if(r){var n=r.name||_.DOM.uniqueId();return v.formatter.register(n,r),{title:r.title,format:n}}}var o,i,c,s,a,u,l,f,m,p;return null}var e,r,n,t,i=(e=[],r=[],n={},{addItemToGroup:function(t,e){n[t]?n[t].push(e):(r.push(t),n[t]=[e])},addItem:function(t){e.push(t)},toFormats:function(){return function(t){for(var e=[],r=0,n=t.length;r<n;++r){if(!f(t[r]))throw new Error("Arr.flatten item "+r+" was not an array, input: "+t);P.apply(e,t[r])}return e}(function(t,e){for(var r=t.length,n=new Array(r),o=0;o<r;o++){var i=t[o];n[o]=e(i,o)}return n}(r,function(t){var e=n[t];return 0===e.length?[]:[{title:t,items:e}]})).concat(e)}}),g={},c=u(v.getParam("importcss_selector_filter")),s=(t=v.getParam("importcss_groups"),x.map(t,function(t){return x.extend({},t,{original:t,selectors:{},filter:u(t.filter)})}));x.each(function(c,t,s){var a=[],r={},u=function(t,e){var r,n,o=n=t.href,i=p.cacheSuffix;if((n=o=l(o)?o.replace("?"+i,"").replace("&"+i,""):o)&&s(n,e)&&!function(t,e){var r,n=!1!==(r=t.getParam("skin"))&&(r||"oxide");if(n){var o=t.getParam("skin_url"),i=o?t.documentBaseURI.toAbsolute(o):m.baseURL+"/skins/ui/"+n,c=m.baseURL+"/skins/content/";return e===i+"/content"+(t.inline?".inline":"")+".min.css"||-1!==e.indexOf(c)}}(c,n)){x.each(t.imports,function(t){u(t,!0)});try{r=t.cssRules||t.rules}catch(t){}x.each(r,function(t){t.styleSheet?u(t.styleSheet,!0):t.selectorText&&x.each(t.selectorText.split(","),function(t){a.push(x.trim(t))})})}};x.each(c.contentCSS,function(t){r[t]=!0}),s=s||function(t,e){return e||r[t]};try{x.each(t.styleSheets,function(t){u(t)})}catch(t){}return a}(v,v.getDoc(),u(v.getParam("importcss_file_filter"))),function(r){var t,e,n;T.test(r)||c&&!c(r)||(n=r,0<(t=x.grep(s,function(t){return!t.filter||t.filter(n)})).length?x.each(t,function(t){var e=o(r,t);e&&i.addItemToGroup(t.title,e)}):(e=o(r,null))&&i.addItem(e))});var a=i.toFormats();v.fire("addStyleModifications",{items:a,replace:!v.getParam("importcss_append")})})}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),l=t("string"),f=t("array"),_=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),m=tinymce.util.Tools.resolve("tinymce.EditorManager"),p=tinymce.util.Tools.resolve("tinymce.Env"),x=tinymce.util.Tools.resolve("tinymce.util.Tools"),P=Array.prototype.push,T=/^\.(?:ephox|tiny-pageembed|mce)(?:[.-]+\w+)+$/;e.add("importcss",function(t){return r(t),e=t,{convertSelectorToFormat:function(t){return h(e,t)}};var e})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function l(e){return e.getParam("insertdatetime_timeformat",e.translate("%H:%M:%S"))}function s(e){return e.getParam("insertdatetime_formats",["%H:%M:%S","%Y-%m-%d","%I:%M:%S %p","%D"])}function r(e,t){if((e=""+e).length<t)for(var n=0;n<t-e.length;n++)e="0"+e;return e}function d(e,t,n){return void 0===n&&(n=new Date),(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=t.replace("%D","%m/%d/%Y")).replace("%r","%I:%M:%S %p")).replace("%Y",""+n.getFullYear())).replace("%y",""+n.getYear())).replace("%m",r(n.getMonth()+1,2))).replace("%d",r(n.getDate(),2))).replace("%H",""+r(n.getHours(),2))).replace("%M",""+r(n.getMinutes(),2))).replace("%S",""+r(n.getSeconds(),2))).replace("%I",""+((n.getHours()+11)%12+1))).replace("%p",n.getHours()<12?"AM":"PM")).replace("%B",""+e.translate(u[n.getMonth()]))).replace("%b",""+e.translate(o[n.getMonth()]))).replace("%A",""+e.translate(i[n.getDay()]))).replace("%a",""+e.translate(a[n.getDay()]))).replace("%%","%")}function p(e,t){var n,r,a,i,o,u;e.getParam("insertdatetime_element",!1)?(n=d(e,t),r=/%[HMSIp]/.test(t)?d(e,"%Y-%m-%dT%H:%M"):d(e,"%Y-%m-%d"),(a=e.dom.getParent(e.selection.getStart(),"time"))?(o=a,u=(i=e).dom.create("time",{datetime:r},n),o.parentNode.insertBefore(u,o),i.dom.remove(o),i.selection.select(u,!0),i.selection.collapse(!1)):e.insertContent('<time datetime="'+r+'">'+n+"</time>")):e.insertContent(d(e,t))}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),a="Sun Mon Tue Wed Thu Fri Sat Sun".split(" "),i="Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday".split(" "),o="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),u="January February March April May June July August September October November December".split(" "),g=tinymce.util.Tools.resolve("tinymce.util.Tools");e.add("insertdatetime",function(e){var n,r,t,a,i,o,u,c;function m(e){return r.execCommand("mceInsertDate",!1,e)}(n=e).addCommand("mceInsertDate",function(e,t){p(n,null!=t?t:n.getParam("insertdatetime_dateformat",n.translate("%Y-%m-%d")))}),n.addCommand("mceInsertTime",function(e,t){p(n,null!=t?t:l(n))}),u=s(r=e),t=0<(o=s(i=r)).length?o[0]:l(i),a=t,c={get:function(){return a},set:function(e){a=e}},r.ui.registry.addSplitButton("insertdatetime",{icon:"insert-time",tooltip:"Insert date/time",select:function(e){return e===c.get()},fetch:function(e){e(g.map(u,function(e){return{type:"choiceitem",text:d(r,e),value:e}}))},onAction:function(e){m(c.get())},onItemAction:function(e,t){c.set(t),m(t)}}),r.ui.registry.addNestedMenuItem("insertdatetime",{icon:"insert-time",text:"Date/time",getSubmenuItems:function(){return g.map(u,function(e){return{type:"menuitem",text:d(r,e),onAction:(t=e,function(){c.set(t),m(t)})};var t})}})})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),l=tinymce.util.Tools.resolve("tinymce.util.Tools");e.add("legacyoutput",function(e){var s,t;(t=s=e).settings.inline_styles=!1,t.getParam("fontsize_formats")||(t.settings.fontsize_formats="8pt=1 10pt=2 12pt=3 14pt=4 18pt=5 24pt=6 36pt=7"),t.getParam("font_formats")||(t.settings.font_formats="Andale Mono=andale mono,monospace;Arial=arial,helvetica,sans-serif;Arial Black=arial black,sans-serif;Book Antiqua=book antiqua,palatino,serif;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,palatino,serif;Helvetica=helvetica,arial,sans-serif;Impact=impact,sans-serif;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco,monospace;Times New Roman=times new roman,times,serif;Trebuchet MS=trebuchet ms,geneva,sans-serif;Verdana=verdana,geneva,sans-serif;Webdings=webdings;Wingdings=wingdings,zapf dingbats"),s.on("PreInit",function(){var e=s,t="p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table",i=l.explode(e.getParam("font_size_style_values","xx-small,x-small,small,medium,large,x-large,xx-large")),a=e.schema;e.formatter.register({alignleft:{selector:t,attributes:{align:"left"}},aligncenter:{selector:t,attributes:{align:"center"}},alignright:{selector:t,attributes:{align:"right"}},alignjustify:{selector:t,attributes:{align:"justify"}},bold:[{inline:"b",remove:"all",preserve_attributes:["class","style"]},{inline:"strong",remove:"all",preserve_attributes:["class","style"]},{inline:"span",styles:{fontWeight:"bold"}}],italic:[{inline:"i",remove:"all",preserve_attributes:["class","style"]},{inline:"em",remove:"all",preserve_attributes:["class","style"]},{inline:"span",styles:{fontStyle:"italic"}}],underline:[{inline:"u",remove:"all",preserve_attributes:["class","style"]},{inline:"span",styles:{textDecoration:"underline"},exact:!0}],strikethrough:[{inline:"strike",remove:"all",preserve_attributes:["class","style"]},{inline:"span",styles:{textDecoration:"line-through"},exact:!0}],fontname:{inline:"font",toggle:!1,attributes:{face:"%value"}},fontsize:{inline:"font",toggle:!1,attributes:{size:function(e){return String(l.inArray(i,e.value)+1)}}},forecolor:{inline:"font",attributes:{color:"%value"},links:!0,remove_similar:!0,clear_child_styles:!0},hilitecolor:{inline:"font",styles:{backgroundColor:"%value"},links:!0,remove_similar:!0,clear_child_styles:!0}}),l.each("b,i,u,strike".split(","),function(e){a.addValidElements(e+"[*]")}),a.getElementRule("font")||a.addValidElements("font[face|size|color|style]"),l.each(t.split(","),function(e){var t=a.getElementRule(e);t&&(t.attributes.align||(t.attributes.align={},t.attributesOrder.push("align")))})})})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function o(n,e){for(var a="",o=0;o<e;o++)a+=n;return a}function s(n,e){var a=n.getParam("nonbreaking_wrap",!0,"boolean")||n.plugins.visualchars?'<span class="'+(n.plugins.visualchars&&n.plugins.visualchars.isEnabled()?"mce-nbsp-wrap mce-nbsp":"mce-nbsp-wrap")+'" contenteditable="false">'+o(" ",e)+"</span>":o(" ",e);n.undoManager.transact(function(){return n.insertContent(a)})}var n=tinymce.util.Tools.resolve("tinymce.PluginManager"),c=tinymce.util.Tools.resolve("tinymce.util.VK");n.add("nonbreaking",function(n){var e,a,o,t,i;function r(){return a.execCommand("mceNonBreaking")}(e=n).addCommand("mceNonBreaking",function(){s(e,1)}),(a=n).ui.registry.addButton("nonbreaking",{icon:"non-breaking",tooltip:"Nonbreaking space",onAction:r}),a.ui.registry.addMenuItem("nonbreaking",{icon:"non-breaking",text:"Nonbreaking space",onAction:r}),0<(i="boolean"==typeof(t=(o=n).getParam("nonbreaking_force_tab",0))?!0===t?3:0:t)&&o.on("keydown",function(n){n.keyCode!==c.TAB||n.isDefaultPrevented()||n.shiftKey||(n.preventDefault(),n.stopImmediatePropagation(),s(o,i))})})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function l(t){return t.getParam("noneditable_noneditable_class","mceNonEditable")}function u(e){return function(t){return-1!==(" "+t.attr("class")+" ").indexOf(e)}}function e(e){var t,r="contenteditable",n=" "+f.trim(e.getParam("noneditable_editable_class","mceEditable"))+" ",a=" "+f.trim(l(e))+" ",i=u(n),o=u(a),c=(t=e.getParam("noneditable_regexp",[]))&&t.constructor===RegExp?[t]:t;e.on("PreInit",function(){0<c.length&&e.on("BeforeSetContent",function(t){!function(t,e,n){var r=e.length,a=n.content;if("raw"!==n.format){for(;r--;)a=a.replace(e[r],function(i,o,c){return function(t){var e=arguments,n=e[e.length-2],r=0<n?o.charAt(n-1):"";if('"'===r)return t;if(">"===r){var a=o.lastIndexOf("<",n);if(-1!==a&&-1!==o.substring(a,n).indexOf('contenteditable="false"'))return t}return'<span class="'+c+'" data-mce-content="'+i.dom.encode(e[0])+'">'+i.dom.encode("string"==typeof e[1]?e[1]:e[0])+"</span>"}}(t,a,l(t)));n.content=a}}(e,c,t)}),e.parser.addAttributeFilter("class",function(t){for(var e,n=t.length;n--;)e=t[n],i(e)?e.attr(r,"true"):o(e)&&e.attr(r,"false")}),e.serializer.addAttributeFilter(r,function(t){for(var e,n=t.length;n--;)e=t[n],(i(e)||o(e))&&(0<c.length&&e.attr("data-mce-content")?(e.name="#text",e.type=3,e.raw=!0,e.value=e.attr("data-mce-content")):e.attr(r,null))})})}var t=tinymce.util.Tools.resolve("tinymce.PluginManager"),f=tinymce.util.Tools.resolve("tinymce.util.Tools");t.add("noneditable",function(t){e(t)})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function u(e){return e.getParam("pagebreak_split_block",!1)}function l(e){var a='<img src="'+n.transparentSrc+'" class="'+m+'" data-mce-resize="false" data-mce-placeholder />';return e?"<p>"+a+"</p>":a}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),n=tinymce.util.Tools.resolve("tinymce.Env"),m="mce-pagebreak";e.add("pagebreak",function(e){var a,n,o,i,t,r;function c(){return n.execCommand("mcePageBreak")}function g(){return u(o)}(a=e).addCommand("mcePageBreak",function(){a.insertContent(l(u(a)))}),(n=e).ui.registry.addButton("pagebreak",{icon:"page-break",tooltip:"Page break",onAction:c}),n.ui.registry.addMenuItem("pagebreak",{text:"Page break",icon:"page-break",onAction:c}),i=(o=e).getParam("pagebreak_separator","\x3c!-- pagebreak --\x3e"),t=new RegExp(i.replace(/[\?\.\*\[\]\(\)\{\}\+\^\$\:]/g,function(e){return"\\"+e}),"gi"),o.on("BeforeSetContent",function(e){e.content=e.content.replace(t,l(g()))}),o.on("PreInit",function(){o.serializer.addNodeFilter("img",function(e){for(var a,n,t,r=e.length;r--;)(t=(n=e[r]).attr("class"))&&-1!==t.indexOf(m)&&(a=n.parent,o.schema.getBlockElements()[a.name]&&g()?(a.type=3,a.value=i,a.raw=!0,n.remove()):(n.type=3,n.value=i,n.raw=!0))})}),(r=e).on("ResolveName",function(e){"IMG"===e.target.nodeName&&r.dom.hasClass(e.target,m)&&(e.name="pagebreak")})})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),f=tinymce.util.Tools.resolve("tinymce.Env"),w=tinymce.util.Tools.resolve("tinymce.util.Tools");e.add("preview",function(e){var n,t;function i(){return t.execCommand("mcePreview")}(n=e).addCommand("mcePreview",function(){var e,t;t=function(t){var n="",i=t.dom.encode,e=t.getParam("content_style","","string");n+='<base href="'+i(t.documentBaseURI.getURI())+'">';var o=t.getParam("content_css_cors",!1,"boolean")?' crossorigin="anonymous"':"";w.each(t.contentCSS,function(e){n+='<link type="text/css" rel="stylesheet" href="'+i(t.documentBaseURI.toAbsolute(e))+'"'+o+">"}),e&&(n+='<style type="text/css">'+e+"</style>");var a,r,s,c,d,l,m,y=-1===(c=(a=t).getParam("body_id","tinymce","string")).indexOf("=")?c:(s=(r=a).getParam("body_id","","hash"))[r.id]||s,u=-1===(m=(d=t).getParam("body_class","","string")).indexOf("=")?m:(l=d).getParam("body_class","","hash")[l.id]||"",v='<script>document.addEventListener && document.addEventListener("click", function(e) {for (var elm = e.target; elm; elm = elm.parentNode) {if (elm.nodeName === "A" && !('+(f.mac?"e.metaKey":"e.ctrlKey && !e.altKey")+")) {e.preventDefault();}}}, false);<\/script> ",g=t.getBody().dir,p=g?' dir="'+i(g)+'"':"";return"<!DOCTYPE html><html><head>"+n+'</head><body id="'+i(y)+'" class="mce-content-body '+i(u)+'"'+p+">"+t.getContent()+v+"</body></html>"}(e=n),e.windowManager.open({title:"Preview",size:"large",body:{type:"panel",items:[{name:"preview",type:"iframe",sandboxed:!0}]},buttons:[{type:"cancel",name:"close",text:"Close",primary:!0}],initialData:{preview:t}}).focus("close")}),(t=e).ui.registry.addButton("preview",{icon:"preview",tooltip:"Preview",onAction:i}),t.ui.registry.addMenuItem("preview",{icon:"preview",text:"Preview",onAction:i})})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";var n=tinymce.util.Tools.resolve("tinymce.PluginManager"),r=tinymce.util.Tools.resolve("tinymce.Env");n.add("print",function(n){var t,i;function e(){return i.execCommand("mcePrint")}(t=n).addCommand("mcePrint",function(){r.browser.isIE()?t.getDoc().execCommand("print",!1,null):t.getWin().print()}),(i=n).ui.registry.addButton("print",{icon:"print",tooltip:"Print",onAction:e}),i.ui.registry.addMenuItem("print",{text:"Print...",icon:"print",onAction:e}),n.addShortcut("Meta+P","","mcePrint")})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function o(e){return e.getParam("save_enablewhendirty",!0)}function a(e,n){e.notificationManager.open({text:n,type:"error"})}function t(t){t.addCommand("mceSave",function(){!function(e){var n=c.DOM.getParent(e.id,"form");if(!o(e)||e.isDirty()){if(e.save(),e.getParam("save_onsavecallback"))return e.execCallback("save_onsavecallback",e),e.nodeChanged();n?(e.setDirty(!1),n.onsubmit&&!n.onsubmit()||("function"==typeof n.submit?n.submit():a(e,"Error: Form submit field collision.")),e.nodeChanged()):a(e,"Error: No form element found.")}}(t)}),t.addCommand("mceCancel",function(){var e=t,n=r.trim(e.startContent);e.getParam("save_oncancelcallback")?e.execCallback("save_oncancelcallback",e):e.resetContent(n)})}function i(t){return function(e){function n(){e.setDisabled(o(t)&&!t.isDirty())}return n(),t.on("NodeChange dirty",n),function(){return t.off("NodeChange dirty",n)}}}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),c=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),r=tinymce.util.Tools.resolve("tinymce.util.Tools");e.add("save",function(e){var n;(n=e).ui.registry.addButton("save",{icon:"save",tooltip:"Save",disabled:!0,onAction:function(){return n.execCommand("mceSave")},onSetup:i(n)}),n.ui.registry.addButton("cancel",{icon:"cancel",tooltip:"Cancel",disabled:!0,onAction:function(){return n.execCommand("mceCancel")},onSetup:i(n)}),n.addShortcut("Meta+S","","mceSave"),t(e)})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function n(e){e.keyCode!==y.TAB||e.ctrlKey||e.altKey||e.metaKey||e.preventDefault()}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),s=tinymce.util.Tools.resolve("tinymce.EditorManager"),o=tinymce.util.Tools.resolve("tinymce.Env"),a=tinymce.util.Tools.resolve("tinymce.util.Delay"),c=tinymce.util.Tools.resolve("tinymce.util.Tools"),y=tinymce.util.Tools.resolve("tinymce.util.VK"),d=t.DOM;e.add("tabfocus",function(e){function t(l){var r,e,t,n,i;l.keyCode!==y.TAB||l.ctrlKey||l.altKey||l.metaKey||l.isDefaultPrevented()||(e=function(e){function t(e){return/INPUT|TEXTAREA|BUTTON/.test(e.tagName)&&s.get(l.id)&&-1!==e.tabIndex&&i(e)}var n=d.select(":input:enabled,*[tabindex]:not(iframe)"),i=function(e){return"BODY"===e.nodeName||"hidden"!==e.type&&"none"!==e.style.display&&"hidden"!==e.style.visibility&&i(e.parentNode)};if(c.each(n,function(e,t){if(e.id===u.id)return r=t,!1}),0<e){for(var o=r+1;o<n.length;o++)if(t(n[o]))return n[o]}else for(o=r-1;0<=o;o--)if(t(n[o]))return n[o];return null},1===(t=c.explode(u.getParam("tab_focus",u.getParam("tabfocus_elements",":prev,:next")))).length&&(t[1]=t[0],t[0]=":prev"),(n=l.shiftKey?":prev"===t[0]?e(-1):d.get(t[0]):":next"===t[1]?e(1):d.get(t[1]))&&(i=s.get(n.id||n.name),n.id&&i?i.focus():a.setTimeout(function(){o.webkit||window.focus(),n.focus()},10),l.preventDefault()))}var u;(u=e).on("init",function(){u.inline&&d.setAttrib(u.getBody(),"tabIndex",null),u.on("keyup",n),o.gecko?u.on("keypress keydown",t):u.on("keydown",t)})})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("textcolor",function(){})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function a(t){return t.getParam("toc_class","mce-toc")}function s(t){var e=t.getParam("toc_header","h2");return/^h[1-6]$/.test(e)?e:"h2"}function u(n){return function(t){function e(){return t.setDisabled(n.mode.isReadOnly()||!(0<g(n).length))}return e(),n.on("LoadContent SetContent change",e),function(){return n.on("LoadContent SetContent change",e)}}}function d(t){var e,n,o,i="",r=g(t),c=function(t){for(var e=9,n=0;n<t.length;n++)if(1===(e=t[n].level<e?t[n].level:e))return e;return e}(r)-1;if(!r.length)return"";i+=(e=s(t),n=v.translate("Table of Contents"),o="</"+e+">","<"+e+' contenteditable="true">'+m.DOM.encode(n)+o);for(var l=0;l<r.length;l++){var a=r[l];a.element.id=a.id;var u=r[l+1]&&r[l+1].level;if(c===a.level)i+="<li>";else for(var d=c;d<a.level;d++)i+="<ul><li>";if(i+='<a href="#'+a.id+'">'+a.title+"</a>",u!==a.level&&u)for(d=a.level;u<d;d--)i+=d===u+1?"</li></ul><li>":"</li></ul>";else i+="</li>",u||(i+="</ul>");c=a.level}return i}function f(t){var e=a(t),n=t.$("."+e);n.length&&t.undoManager.transact(function(){n.html(d(t))})}var t,e=tinymce.util.Tools.resolve("tinymce.PluginManager"),m=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),v=tinymce.util.Tools.resolve("tinymce.util.I18n"),c=tinymce.util.Tools.resolve("tinymce.util.Tools"),l=(t=0,function(){return"mcetoc_"+(new Date).getTime().toString(32)+(t++).toString(32)}),g=function(n){var t,o=a(n),e=s(n),i=function(t){for(var e=[],n=1;n<=t;n++)e.push("h"+n);return e.join(",")}(1<=(t=parseInt(n.getParam("toc_depth","3"),10))&&t<=9?t:3),r=n.$(i);return r.length&&/^h[1-9]$/i.test(e)&&(r=r.filter(function(t,e){return!n.dom.hasClass(e.parentNode,o)})),c.map(r,function(t){return{id:t.id||l(),level:parseInt(t.nodeName.replace(/^H/i,""),10),title:n.$.text(t),element:t}})};e.add("toc",function(t){var c,e,n,o,i,r;function l(){return i.execCommand("mceInsertToc")}(c=t).addCommand("mceInsertToc",function(){var t,e,n,o,i,r;o=a(t=c),i=t.$("."+o),r=t,!i.length||0<r.dom.getParents(i[0],".mce-offscreen-selection").length?t.insertContent((n=d(e=t),'<div class="'+e.dom.encode(a(e))+'" contenteditable="false">'+n+"</div>")):f(t)}),c.addCommand("mceUpdateToc",function(){f(c)}),(i=t).ui.registry.addButton("toc",{icon:"toc",tooltip:"Table of contents",onAction:l,onSetup:u(i)}),i.ui.registry.addButton("tocupdate",{icon:"reload",tooltip:"Update",onAction:function(){return i.execCommand("mceUpdateToc")}}),i.ui.registry.addMenuItem("toc",{icon:"toc",text:"Table of contents",onAction:l,onSetup:u(i)}),i.ui.registry.addContextToolbar("toc",{items:"tocupdate",predicate:(r=i,function(t){return t&&r.dom.is(t,"."+a(r))&&r.getBody().contains(t)}),scope:"node",position:"node"}),n=(e=t).$,o=a(e),e.on("PreProcess",function(t){var e=n("."+o,t.node);e.length&&(e.removeAttr("contentEditable"),e.find("[contenteditable]").removeAttr("contentEditable"))}),e.on("SetContent",function(){var t=n("."+o);t.length&&(t.attr("contentEditable",!1),t.children(":first-child").attr("contentEditable",!0))})})}();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function f(t,o,e){var n,i;t.dom.toggleClass(t.getBody(),"mce-visualblocks"),e.set(!e.get()),n=t,i=e.get(),n.fire("VisualBlocks",{state:i})}function g(e,n){return function(o){function t(t){return o.setActive(t.state)}return o.setActive(n.get()),e.on("VisualBlocks",t),function(){return e.off("VisualBlocks",t)}}}tinymce.util.Tools.resolve("tinymce.PluginManager").add("visualblocks",function(t,o){var e,n,i,s,c,u,l,a=(e=!1,{get:function(){return e},set:function(t){e=t}});function r(){return s.execCommand("mceVisualBlocks")}i=a,(n=t).addCommand("mceVisualBlocks",function(){f(n,0,i)}),(s=t).ui.registry.addToggleButton("visualblocks",{icon:"visualblocks",tooltip:"Show blocks",onAction:r,onSetup:g(s,c=a)}),s.ui.registry.addToggleMenuItem("visualblocks",{text:"Show blocks",icon:"visualblocks",onAction:r,onSetup:g(s,c)}),l=a,(u=t).on("PreviewFormats AfterPreviewFormats",function(t){l.get()&&u.dom.toggleClass(u.getBody(),"mce-visualblocks","afterpreviewformats"===t.type)}),u.on("init",function(){u.getParam("visualblocks_default_state",!1,"boolean")&&f(u,0,l)})})}();
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*/
|
||||
body{background-color:#2f3742;color:#dfe0e4;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}a{color:#4099ff}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#6d737b}figure{display:table;margin:1rem auto}figure figcaption{color:#8a8f97;display:block;margin-top:.25rem;text-align:center}hr{border-color:#6d737b;border-style:solid;border-width:1px 0 0 0}code{background-color:#6d737b;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #6d737b;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #6d737b;margin-right:1.5rem;padding-right:1rem}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*/
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*/
|
||||
@media screen{html{background:#f4f4f4;min-height:100%}}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif}@media screen{body{background-color:#fff;box-shadow:0 0 4px rgba(0,0,0,.15);box-sizing:border-box;margin:1rem auto 0;max-width:820px;min-height:calc(100vh - 1rem);padding:4rem 6rem 6rem 6rem}}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure figcaption{color:#999;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*/
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem auto;max-width:900px}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}
|
||||