Compare commits
617 Commits
feature/it
...
563b240fbf
| Author | SHA1 | Date | |
|---|---|---|---|
| 563b240fbf | |||
| e75d8f9b25 | |||
| 4ea03922a3 | |||
| 295585d8b6 | |||
| e7263feecf | |||
| 8250eaf2b5 | |||
| 72a2a3e9a9 | |||
| 31f523c88f | |||
| a1fb0d4f9b | |||
| fe930b5831 | |||
| 899493a74d | |||
| 45ad99cb38 | |||
| 10c6e20db4 | |||
| 50e4c72c8a | |||
| eb18a3facb | |||
| 9fc979e135 | |||
| fa7efb7b24 | |||
|
|
bec933b3b4 | ||
|
|
1675f3edcf | ||
|
|
2fe47c86d3 | ||
|
|
00a6209347 | ||
| c18c68b6b7 | |||
| 03d129c32c | |||
| d6e3131c6a | |||
| 1d3805781c | |||
| b45c35a5e8 | |||
| b05e19e9f8 | |||
| 4331b84a63 | |||
| 0b81e9c1dd | |||
| f653960a30 | |||
| 888fae119f | |||
| f503e20030 | |||
| 0166601be8 | |||
| 83a23701a7 | |||
| bedfd1f559 | |||
| 8bcabafd08 | |||
| 5ff5093d7b | |||
|
|
23fa9c0ea2 | ||
|
|
cde9333652 | ||
|
|
7bb8699403 | ||
|
|
1bccaffe27 | ||
| 7a8d946960 | |||
| d1c530fdc1 | |||
| 0f53b407db | |||
| 0da6586bb6 | |||
| 2c87ac535a | |||
| 9ae2210388 | |||
| 33f763b48f | |||
|
|
8c0a655906 | ||
|
|
5e8cc4d0a6 | ||
|
|
a8b219e880 | ||
|
|
d38f299c4b | ||
|
|
f4a7374f8c | ||
|
|
9d66d554ec | ||
| ea342a225c | |||
| ba5e85ba37 | |||
| 7527841fe0 | |||
|
|
13d27553b9 | ||
|
|
b1686aaf66 | ||
| 3f0a3584ec | |||
| 2777ecf664 | |||
|
|
dcec94278c | ||
|
|
31f6f7c29f | ||
|
|
7aefbafb6f | ||
|
|
a83a8298d2 | ||
|
|
c6281d0559 | ||
|
|
7af1c75eea | ||
|
|
77e0e81a5c | ||
|
|
23135ff01a | ||
|
|
e094c5ae49 | ||
|
|
dc7e152311 | ||
|
|
81a016ada9 | ||
|
|
8d8e2be001 | ||
|
|
8f9507a665 | ||
|
|
27a7773d95 | ||
|
|
1675bcbedf | ||
|
|
bf857b2820 | ||
|
|
bc2b852f98 | ||
|
|
6c1f07da2c | ||
|
|
8538256edf | ||
|
|
58f1b2fa78 | ||
|
|
0f97d53344 | ||
|
|
c44f10d1e1 | ||
|
|
86dcd23df7 | ||
|
|
0b41b9f813 | ||
| 5a085459f8 | |||
| 4dc0644f8d | |||
| 6a0040d0a3 | |||
| 41b326eb3a | |||
| f8294509e5 | |||
|
|
1e5c341966 | ||
| 16a0a421c2 | |||
| 63f22e2538 | |||
| b3e7ef63f6 | |||
|
|
49d07914fd | ||
|
|
7809285b1d | ||
|
|
0d4393fc34 | ||
| 830567d6c9 | |||
| 1a4a009543 | |||
| 4681a2a6a4 | |||
| be3a6c0596 | |||
| 446243910f | |||
| bd7cb4c301 | |||
|
|
b8dfa3d887 | ||
| e2988e91a1 | |||
| 7c588ee58c | |||
| ee21fe9195 | |||
| 6a469181cd | |||
| ec492e3829 | |||
| 3e4ad775a6 | |||
|
|
8f4a7ee842 | ||
|
|
718be1cfdb | ||
|
|
07374c826c | ||
| 55a3c597eb | |||
| 786065d758 | |||
| 6c3572e568 | |||
|
|
f5362e6887 | ||
|
|
6604695674 | ||
| b4ceac9ad1 | |||
| eb4a66329e | |||
| 9afb850a7b | |||
| 559af1334b | |||
| 90ff585a2e | |||
| a19263334e | |||
| e5b706249a | |||
| 80ed9803be | |||
| b5f5ce591f | |||
| f5fbe1efc8 | |||
| b9e1b07b3c | |||
| 7d7d5356ff | |||
| bb4acac3c1 | |||
| c369f9142f | |||
| 77cad7a83a | |||
|
|
e15de71f52 | ||
|
|
4a0fcf77e6 | ||
|
|
293272ef83 | ||
|
|
021d31b6b4 | ||
|
|
b28988f15f | ||
|
|
6971336477 | ||
| 51da042beb | |||
| 6d66f8deee | |||
| f35df29264 | |||
| 0784b2a40e | |||
| 463da04038 | |||
|
|
352171c019 | ||
|
|
1b8a2d8127 | ||
|
|
f4c0df3579 | ||
|
|
aa9404a146 | ||
| 4c4f0678d2 | |||
|
|
ceeeeb1ef4 | ||
| 30ca2afca8 | |||
|
|
5f956540e8 | ||
|
|
012a661a19 | ||
| 6d934b4418 | |||
| 37215a7758 | |||
| 492e4c02aa | |||
| 77516a4dff | |||
| 43486d9cc3 | |||
| f695977cbc | |||
| 5b987d057b | |||
|
|
f344dc7d00 | ||
|
|
71352923c8 | ||
|
|
a2c3e4c41e | ||
| b8dcb69e47 | |||
| 680fe057e7 | |||
|
|
7f39f3066f | ||
|
|
7ce4efa146 | ||
|
|
95c9686597 | ||
|
|
e4b25e2648 | ||
| ffe0ebad35 | |||
| 841c284586 | |||
| 4644ae298d | |||
| ebd6abc147 | |||
| 935b222602 | |||
| 1b711fa6e3 | |||
| 14af77ca65 | |||
|
|
cbb38d48b9 | ||
|
|
31be9d4a25 | ||
|
|
3aeaaa76a8 | ||
|
|
2b8a19b4af | ||
| 75e2f50714 | |||
|
|
8d685109d3 | ||
|
|
020d74f36c | ||
| 87a9ac9092 | |||
| 90e1d428c4 | |||
| fcd5408052 | |||
| 91100ca635 | |||
|
|
4decb99856 | ||
| 4b2f82664b | |||
|
|
113d82c254 | ||
|
|
ec0d97867f | ||
| d2e07616fe | |||
| be32d224b7 | |||
| 463110f905 | |||
| d1e805a88d | |||
| 911b6ca31a | |||
| 0ba7ec25df | |||
|
|
4c36dc5bbe | ||
|
|
5f6830434d | ||
| 434a73ccc0 | |||
| b0d2f2810a | |||
|
|
a38996b751 | ||
| 5104a8b012 | |||
| 8a993bc0c3 | |||
|
|
e14335b635 | ||
| 9b2b708538 | |||
| 973c3a9018 | |||
| df668316c9 | |||
|
|
0db6302652 | ||
| fbe03aaea4 | |||
| de2b7dd6ce | |||
| e508014224 | |||
| 6e5ccca038 | |||
| c6cce7fe61 | |||
|
|
437d5f6834 | ||
| bd7e70f3da | |||
| 12a423051a | |||
|
|
0643d56194 | ||
| 14b84cc08d | |||
| c82724153f | |||
| 58700c1097 | |||
| 2f3f7a486c | |||
| 6d8116713f | |||
| 2ad27d738f | |||
|
|
4d79b178e3 | ||
| 9aae26b4f2 | |||
| bff1a2fd07 | |||
|
|
3ea6a57a10 | ||
| a9ae162c90 | |||
| 6a32400118 | |||
| ce36101929 | |||
|
|
55e0791e16 | ||
| a99c215db0 | |||
| 318cc415ed | |||
| 53b4f43b14 | |||
| d014227e9c | |||
|
|
f320ec7d37 | ||
| 7bd1269aad | |||
|
|
f3b07ac875 | ||
| 61bf95b58e | |||
| 1fca5ed477 | |||
| a523bb482e | |||
| cbb16388fe | |||
| 98498918ed | |||
| 393a092653 | |||
| a8591c438e | |||
| b2085a84ca | |||
| 473cfa0052 | |||
|
|
f456e7bee0 | ||
| 94ee2e9ad6 | |||
|
|
5344bfc426 | ||
| 9d6cba6e01 | |||
|
|
666eb6bcc6 | ||
| 2dc4fbd213 | |||
|
|
c2ed71540f | ||
| b228335446 | |||
|
|
e453753bdd | ||
|
|
881f4668da | ||
| fe851e37c4 | |||
|
|
16a349292b | ||
|
|
dc0ce471aa | ||
| b57fc31297 | |||
| f1e369df9f | |||
|
|
efcc645e24 | ||
|
|
32d6e3bbbd | ||
|
|
2639724f9f | ||
| 07dd52aa7b | |||
|
|
cc1b813a9d | ||
| 84363475b0 | |||
| f1c4ab62bf | |||
| abd243fce2 | |||
|
|
b587460449 | ||
| 8902cdcfd5 | |||
| 743467c300 | |||
|
|
bb7e7a75e9 | ||
| 9abb7b65be | |||
| a7fb6c67d1 | |||
| 8250420d99 | |||
|
|
c1b63b850a | ||
| 3500e3f520 | |||
| b5095602ca | |||
| 5af6500671 | |||
|
|
17c16028b1 | ||
|
|
e111f7b362 | ||
|
|
f0987127eb | ||
|
|
ca6247286a | ||
|
|
1a69324d59 | ||
| e684c495ee | |||
| 9162f8fb6d | |||
| fc8d29e513 | |||
| d0634bb2e7 | |||
| a679e2695d | |||
|
|
2315ffe64e | ||
| e25f4b4681 | |||
| 9efaef5950 | |||
|
|
9f7f55aeff | ||
| cea7900c5e | |||
|
|
103a2b9f03 | ||
|
|
3ef9570f3b | ||
| dc2afc4260 | |||
| a486977b80 | |||
| 5c8fe8e04c | |||
|
|
a1f4c82cec | ||
|
|
8a5cbde5ef | ||
|
|
3fc63d0b3e | ||
|
|
106ce09482 | ||
|
|
4014b3fb84 | ||
|
|
a5578bf669 | ||
| 099700758c | |||
|
|
58fb9fce44 | ||
| 75ee110cfb | |||
| c4bc5efbc2 | |||
| dbe5f3c004 | |||
|
|
e64d22e2f6 | ||
| 13f6bce2bc | |||
| 0e52e78134 | |||
| 260167c6a8 | |||
| 3dab72701e | |||
| 3e6cdb5ed5 | |||
| 576da0c9be | |||
| 82ae9ab953 | |||
| 44353c09a0 | |||
| e2d32e555b | |||
| db70147468 | |||
| 32a1ed2de7 | |||
| 15b4350051 | |||
| 6bcd298995 | |||
| d2a39de576 | |||
|
|
1f640622e0 | ||
|
|
79b39a3ef6 | ||
|
|
381413a49c | ||
|
|
eb3288dab7 | ||
|
|
e5f0f5da61 | ||
|
|
805063c686 | ||
| 3157fb9401 | |||
| f532f1fb52 | |||
|
|
599b815829 | ||
|
|
bc7ebebef4 | ||
| 2bb53024dc | |||
|
|
bcc5e70c20 | ||
| a07811e36d | |||
|
|
9964ccbc1f | ||
| e2d7fceff6 | |||
|
|
a48937ae52 | ||
|
|
afd7bda269 | ||
| 9aa8983e72 | |||
| e246459a08 | |||
| 8388f1243a | |||
| c4644489e7 | |||
| bd712e562f | |||
| d54309aa92 | |||
| 2a92f48a2c | |||
| 5a5305fb24 | |||
|
|
07aaa32bdf | ||
| 05fd5b32f2 | |||
| 815ed9267e | |||
|
|
55e92bc7b4 | ||
| 6586db4996 | |||
| cf749e0f34 | |||
|
|
1f6b592b9f | ||
| f71a84f06d | |||
| 05b0ba73be | |||
| f9dafbc02c | |||
| 7be8caf3f7 | |||
| 6402a38cb4 | |||
| ff93ab7fa2 | |||
| ada24eef66 | |||
| f79ee8be87 | |||
| e6f4b9b49c | |||
| 3456237a39 | |||
| e6905ca17e | |||
| 82dc28c3fc | |||
|
|
a15132d75d | ||
| a197800d2f | |||
| 80f5ed426c | |||
| 22d16bbb91 | |||
| 49d6e7e271 | |||
| e8683381d9 | |||
| aa0c5d29f0 | |||
| b35da7b8a5 | |||
| cd060ec562 | |||
|
|
0bad332c22 | ||
|
|
bae36f6f43 | ||
|
|
f6551c7e8b | ||
| a0343eec93 | |||
| 9fb5c171eb | |||
| 750f50d953 | |||
| 4428ac860d | |||
|
|
72f1accbe4 | ||
| 307735df62 | |||
| 575bc5a5d3 | |||
| 93c99105c0 | |||
| bdf2bf8beb | |||
| 662a0cc4ac | |||
| e3043a3f0d | |||
| e098fea558 | |||
| f4352ca8c1 | |||
| 297c6b7ef6 | |||
| 82d21e9fe9 | |||
| c34bab591a | |||
| 1934126a18 | |||
|
|
ad063a1f01 | ||
|
|
af5fdcba88 | ||
| d7594d026a | |||
| 1195fc1fa5 | |||
| cde86c1c92 | |||
|
|
e44b3cd6cc | ||
| 9cfd10a265 | |||
|
|
1a0b1c4c48 | ||
| ffb09a8c72 | |||
| e7fb3b1f96 | |||
| c8890c1a85 | |||
| 2d3a7064e3 | |||
| f1183d0a3d | |||
|
|
e48b4df1c6 | ||
| d824c913e8 | |||
| bf11b13aee | |||
| 1ba0561ee9 | |||
| 2e9aa74b72 | |||
| fe845227df | |||
|
|
3b328204a2 | ||
| f8d93e2851 | |||
| b1e444930b | |||
|
|
1575f9e680 | ||
|
|
208f4d08e5 | ||
|
|
19237be4aa | ||
| 207520e1d6 | |||
| cbdafbd4b7 | |||
| 0dd00d17f3 | |||
| 0a133f7890 | |||
| fd951f81f2 | |||
| 14186d98c0 | |||
| 390c1a8450 | |||
| 92af11c787 | |||
| 5a00828568 | |||
|
|
24b65bd6f5 | ||
| d597a70df8 | |||
|
|
269b901e64 | ||
| 98a96be657 | |||
| 777dccc7bd | |||
|
|
9464a368ba | ||
| bae1672f42 | |||
| 6fa69d81f4 | |||
| 81a4d6baf1 | |||
|
|
f2b87ddf0a | ||
|
|
835c06ce94 | ||
| fb1d7bf241 | |||
| e6ef80f17f | |||
|
|
cfa72fe19b | ||
| eae23d4457 | |||
| c101b8bf7e | |||
|
|
4a6149963d | ||
| 373e84248f | |||
|
|
62ef2b1ff9 | ||
| 0e963c0f11 | |||
|
|
6b0ffc810b | ||
| d12e2e0b4c | |||
|
|
09b2c256fb | ||
| 29c257c9f8 | |||
| 8c8d76b6c3 | |||
| d12618f320 | |||
| dac1d9bc2b | |||
| 2465d739fe | |||
|
|
29b4ba8b5b | ||
|
|
61e3a0ed60 | ||
|
|
6f457b28f3 | ||
|
|
36322a0927 | ||
|
|
1d7b028693 | ||
|
|
1a6cde2d36 | ||
|
|
d2f5f3d0b5 | ||
|
|
b59150551e | ||
|
|
736c29a007 | ||
| abc3fea293 | |||
| 98b65a6ca4 | |||
|
|
4d249895ed | ||
| 34deb61632 | |||
|
|
134dec8f9d | ||
|
|
60505f52ea | ||
|
|
ad493bcea6 | ||
| b14ea842f8 | |||
| d863cccd9f | |||
| f529aea087 | |||
| 07828b63f2 | |||
| e998cfa2f8 | |||
| dc0ab88fb9 | |||
| 2d1444b956 | |||
| f6c8610104 | |||
| 0f8f40fc7b | |||
| 6dc91daaca | |||
| ebc7320eeb | |||
|
|
8639eee5df | ||
|
|
e76fac0ab1 | ||
| 87b7aa9d67 | |||
|
|
b08366c3f7 | ||
| 8f02f68390 | |||
| 2f1946a834 | |||
| b30a51e84a | |||
| 60d42b2e2e | |||
| e5851e91b8 | |||
| f67832f228 | |||
| 4d7601abaf | |||
| 8dd49e4fa2 | |||
| 42b0a5778e | |||
| 777872486a | |||
| 8083c0e015 | |||
| fab7d669d5 | |||
| 81f7c5aeac | |||
| e162ad5a12 | |||
| c56c140e4b | |||
|
|
ea8d701a8d | ||
|
|
db47a15544 | ||
| fcba883f42 | |||
| 2b9c70b550 | |||
| 6cd5477eed | |||
| 495e46fc31 | |||
| b9f0e24950 | |||
|
|
d036ce4f42 | ||
| ee9f7a4d81 | |||
|
|
e56b7d53a4 | ||
|
|
8bc4b90fe9 | ||
| 9b1a1e3dc7 | |||
| b9af603cb7 | |||
| e4b5e6ae30 | |||
| ae90bd7c52 | |||
| 626c138fd2 | |||
|
|
b8bd93532c | ||
| d43433295d | |||
| 5db6e59bbc | |||
| dcd79a2863 | |||
|
|
284c19f036 | ||
| b7b8b90398 | |||
| 311ddd9a2e | |||
| 6615f39466 | |||
| d472b771e1 | |||
| 5fa20c837a | |||
| 749f0ce3c3 | |||
| 273d5709cd | |||
| 78e193c8df | |||
| 9d30555265 | |||
|
|
e4af3232dd | ||
| d15a2037d7 | |||
| 8172226d89 | |||
|
|
f92393f898 | ||
| 668cde3b29 | |||
|
|
c4412295fa | ||
| c651e7bc72 | |||
| 2d7809b4e0 | |||
| 12b4259ebc | |||
| fde8726e14 | |||
| ba36c0ec19 | |||
| d797868c17 | |||
| 3d2dea6118 | |||
| 6632943c7e | |||
|
|
0d539628f3 | ||
| 288871cb39 | |||
|
|
9885085259 | ||
| 572ffe81cf | |||
|
|
29e7b41615 | ||
|
|
387672b5b2 | ||
|
|
8812290f8a | ||
| 0d4e6ee7ea | |||
| c367ba4ad9 | |||
| df51cf6852 | |||
| 50a01e1e47 | |||
| ed40569ac9 | |||
| 9a134bc83a | |||
| 14556251f1 | |||
| b52e9c70af | |||
| 1c338f4d3f | |||
| bf08447cd6 | |||
| a74f41228d | |||
| 810a348f31 | |||
|
|
6e483deea8 | ||
|
|
eccfd959fe | ||
|
|
a938da9e22 | ||
|
|
386cd30bc0 | ||
|
|
4b1a3abf05 | ||
| d4e64c290c | |||
| c885844a3a | |||
| 5011bac596 | |||
| 258c8e4179 | |||
| 5ab1354bcc | |||
| 2a14ae72ff | |||
|
|
f8dbc6b2ae | ||
| 7b917fcbcd | |||
| bf558a0243 | |||
| 581dde8679 | |||
| 5d0e453a68 | |||
| ec0ad53837 | |||
| 62bf081adb | |||
| 68babd54be | |||
| 2443c0dc63 | |||
| a45ff9af28 | |||
| 1fcefb1d2b | |||
| f400f01db7 | |||
|
|
d38b1242d7 | ||
|
|
0e5307f7a3 | ||
|
|
388b113b58 | ||
|
|
c749c09dea | ||
| 8af838ab55 | |||
|
|
69832b4c58 | ||
|
|
fb2be8651e | ||
|
|
d957f72198 | ||
|
|
f0c0de2ecd | ||
|
|
41ef0bdd86 | ||
|
|
c1abf89d80 | ||
|
|
d5f758f1eb | ||
|
|
402499718b | ||
|
|
d397399047 | ||
| 64a0e37cc7 | |||
|
|
e0b2ab63e7 | ||
|
|
f0e8e51d06 | ||
| 346fe4c426 | |||
| 2fd92e063f | |||
|
|
2ebcea0255 | ||
| f4fe50fd3b | |||
| e5bea96182 |
16
.claude/hooks/block-build.sh
Executable file
16
.claude/hooks/block-build.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# PreToolUse Hook: 빌드 명령 차단
|
||||
# CLAUDE.md 규칙: "Claude가 직접 npm run build 실행 금지"
|
||||
|
||||
INPUT=$(cat)
|
||||
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
|
||||
|
||||
# 빌드 명령 패턴 체크
|
||||
if echo "$COMMAND" | grep -qE '(npm run build|next build|yarn build|pnpm build)(\s|$|;|&&|\|)'; then
|
||||
echo "🚫 빌드 명령이 차단되었습니다." >&2
|
||||
echo " CLAUDE.md 규칙: Claude가 직접 빌드 실행 금지" >&2
|
||||
echo " 빌드가 필요하면 사용자에게 요청하세요." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exit 0
|
||||
31
.claude/hooks/check-file-size.sh
Executable file
31
.claude/hooks/check-file-size.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
# PreToolUse Hook: Write 전 파일 크기 급감 경고
|
||||
# 기존 파일 대비 50% 이상 줄어들면 경고 (최소 50줄 이상 파일만)
|
||||
|
||||
INPUT=$(cat)
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
||||
NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty')
|
||||
|
||||
# 파일이 없으면 (신규 생성) 통과
|
||||
if [ ! -f "$FILE_PATH" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
EXISTING_LINES=$(wc -l < "$FILE_PATH" 2>/dev/null | tr -d ' ')
|
||||
NEW_LINES=$(echo "$NEW_CONTENT" | wc -l | tr -d ' ')
|
||||
|
||||
# 기존 파일이 50줄 미만이면 체크 스킵
|
||||
if [ "$EXISTING_LINES" -lt 50 ] 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 새 내용이 기존의 50% 미만이면 경고
|
||||
THRESHOLD=$((EXISTING_LINES / 2))
|
||||
if [ "$NEW_LINES" -lt "$THRESHOLD" ] 2>/dev/null; then
|
||||
echo "⚠️ File size drop: $FILE_PATH" >&2
|
||||
echo " 기존: ${EXISTING_LINES}줄 → 새 내용: ${NEW_LINES}줄 (${THRESHOLD}줄 미만)" >&2
|
||||
echo " 파일 내용이 절반 이상 줄었습니다. 의도한 변경인지 확인하세요." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exit 0
|
||||
29
.claude/hooks/check-unused-imports.sh
Executable file
29
.claude/hooks/check-unused-imports.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
# PostToolUse Hook: Edit/Write 후 미사용 import 체크
|
||||
# 단일 파일 eslint로 빠르게 체크 (전체 tsc보다 빠름)
|
||||
|
||||
INPUT=$(cat)
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
||||
|
||||
# TypeScript 파일만 체크
|
||||
if [[ "$FILE_PATH" != *.ts && "$FILE_PATH" != *.tsx ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 파일 존재 확인
|
||||
if [ ! -f "$FILE_PATH" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cd "$CLAUDE_PROJECT_DIR" 2>/dev/null || exit 0
|
||||
|
||||
# eslint로 미사용 변수/import 체크 (단일 파일 → 빠름)
|
||||
RESULT=$(npx eslint --no-eslintrc --rule '{"@typescript-eslint/no-unused-vars": "error"}' --parser @typescript-eslint/parser --plugin @typescript-eslint "$FILE_PATH" 2>&1 | grep "no-unused-vars" | head -10)
|
||||
|
||||
if [ -n "$RESULT" ]; then
|
||||
echo "Unused imports/variables in $FILE_PATH:" >&2
|
||||
echo "$RESULT" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exit 0
|
||||
25
.claude/hooks/typecheck-after-edit.sh
Executable file
25
.claude/hooks/typecheck-after-edit.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# PostToolUse Hook: Write/Edit 후 TypeScript 타입체크
|
||||
# exit 0 = 성공, exit 2 = 에러 (Claude에 피드백)
|
||||
|
||||
INPUT=$(cat)
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
||||
|
||||
# TypeScript 파일만 체크
|
||||
if [[ "$FILE_PATH" != *.ts && "$FILE_PATH" != *.tsx ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 프로젝트 디렉토리로 이동
|
||||
cd "$CLAUDE_PROJECT_DIR" 2>/dev/null || exit 0
|
||||
|
||||
# tsc 실행 (에러만 출력, 최대 20줄)
|
||||
RESULT=$(npx tsc --noEmit 2>&1 | head -20)
|
||||
|
||||
if [ -n "$RESULT" ] && echo "$RESULT" | grep -q "error TS"; then
|
||||
echo "TypeScript errors after editing $FILE_PATH:" >&2
|
||||
echo "$RESULT" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exit 0
|
||||
39
.env.example
39
.env.example
@@ -1,39 +0,0 @@
|
||||
# ==============================================
|
||||
# API Configuration
|
||||
# ==============================================
|
||||
NEXT_PUBLIC_API_URL=https://api.5130.co.kr
|
||||
|
||||
# Frontend URL (for CORS)
|
||||
NEXT_PUBLIC_FRONTEND_URL=http://localhost:3000
|
||||
|
||||
# ==============================================
|
||||
# Authentication Mode
|
||||
# ==============================================
|
||||
# 인증 모드 선택: sanctum | bearer
|
||||
# - sanctum: 웹 브라우저 사용자 (HTTP-only 쿠키)
|
||||
# - bearer: 모바일/SPA (토큰 기반)
|
||||
NEXT_PUBLIC_AUTH_MODE=sanctum
|
||||
|
||||
# ==============================================
|
||||
# API Key (⚠️ 서버 사이드 전용 - 절대 공개 금지!)
|
||||
# ==============================================
|
||||
# 개발팀 공유: 팀 내부 문서에서 키 값 확인
|
||||
# 주기적 갱신: PHP 백엔드 팀에서 새 키 발급 시 업데이트 필요
|
||||
#
|
||||
# ⚠️ 주의사항:
|
||||
# 1. 절대 NEXT_PUBLIC_ 접두사 붙이지 말 것!
|
||||
# 2. Git에 커밋하지 말 것! (.gitignore에 포함됨)
|
||||
# 3. 브라우저에서 접근 불가 (서버 사이드 전용)
|
||||
#
|
||||
# 사용처:
|
||||
# - 서버 간 통신 (Next.js API Routes)
|
||||
# - 백그라운드 작업 (Cron, Scripts)
|
||||
# - 외부 시스템 연동
|
||||
API_KEY=your-secret-api-key-here
|
||||
|
||||
# ==============================================
|
||||
# Development Notes
|
||||
# ==============================================
|
||||
# 1. .env.example을 복사하여 .env.local 생성
|
||||
# 2. .env.local에 실제 키 값 입력
|
||||
# 3. .env.local은 Git에 커밋되지 않음
|
||||
23
.gitignore
vendored
23
.gitignore
vendored
@@ -95,9 +95,10 @@ build/
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
# ---> Claude
|
||||
.env.local
|
||||
.env*.local
|
||||
# ---> Environment
|
||||
.env
|
||||
.env.*
|
||||
.env*
|
||||
|
||||
# ---> Unused components and contexts (archived)
|
||||
src/components/_unused/
|
||||
@@ -109,3 +110,19 @@ playwright.config.ts
|
||||
playwright-report/
|
||||
test-results/
|
||||
.playwright/
|
||||
|
||||
# ---> Build artifacts
|
||||
package-lock.json
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# ---> Dev Page Builder (프로토타입 - 로컬 전용)
|
||||
src/app/**/dev/page-builder/
|
||||
|
||||
# ---> Dev Dashboard Prototypes (디자인 프로토타입 - 로컬 전용)
|
||||
src/app/**/dev/dashboard/
|
||||
|
||||
# ---> Serena MCP memories
|
||||
.serena/
|
||||
|
||||
# ---> Deploy script (로컬 전용)
|
||||
deploy.sh
|
||||
|
||||
BIN
.serena/.DS_Store
vendored
Normal file
BIN
.serena/.DS_Store
vendored
Normal file
Binary file not shown.
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/cache
|
||||
81
.serena/memories/quote-registration-formfield-fix.md
Normal file
81
.serena/memories/quote-registration-formfield-fix.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# 견적 등록/수정 FormField type="custom" 수정 작업
|
||||
|
||||
## 📅 작업일: 2026-01-06
|
||||
|
||||
## 🎯 문제 요약
|
||||
견적 등록/수정 페이지에서 **수량(quantity) 변경이 총합계에 반영되지 않는 버그**
|
||||
|
||||
### 증상
|
||||
- 수량 1 → 자동견적산출 → 합계 1,711,225원
|
||||
- 수량 3으로 변경 → 자동견적산출 → 합계가 여전히 1,711,225원 (3배인 ~5,133,675원이어야 함)
|
||||
|
||||
## 🔍 근본 원인
|
||||
**FormField 컴포넌트의 `type="custom"` 누락**
|
||||
|
||||
FormField 컴포넌트 (`src/components/molecules/FormField.tsx`)는 `type` prop에 따라 다르게 동작:
|
||||
- `type="custom"` → children(자식 요소)을 렌더링
|
||||
- 그 외 → 자체 내부 Input을 렌더링 (value/onChange 연결 안됨)
|
||||
|
||||
```tsx
|
||||
// FormField.tsx 내부 renderInput() 함수
|
||||
case 'custom':
|
||||
return children; // ← children 렌더링
|
||||
default:
|
||||
return <Input value={value} onChange={...} /> // ← 자체 Input 렌더링 (value=undefined)
|
||||
```
|
||||
|
||||
**결과**: `type="custom"` 없이 FormField 안에 Input을 넣으면, 해당 Input은 렌더링되지 않고 FormField 자체 Input이 렌더링됨 → state와 연결 끊김
|
||||
|
||||
## ✅ 수정 완료 (8개 FormField)
|
||||
|
||||
### 파일: `src/components/quotes/QuoteRegistration.tsx`
|
||||
|
||||
**기본정보 섹션** (3개):
|
||||
1. 등록일 (line 581) - `type="custom"` 추가
|
||||
2. 현장명 (line 627) - `type="custom"` 추가 (datalist 자동완성 포함)
|
||||
3. 납기일 (line 662) - `type="custom"` 추가
|
||||
|
||||
**견적 항목 섹션** (5개):
|
||||
4. 층수 (line 733) - `type="custom"` 추가
|
||||
5. 부호 (line 744) - `type="custom"` 추가
|
||||
6. **수량 (line 926)** - `type="custom"` 추가 ⭐ 핵심 버그 원인
|
||||
7. 마구리 날개치수 (line 944) - `type="custom"` 추가
|
||||
8. 검사비 (line 959) - `type="custom"` 추가
|
||||
|
||||
## 추가 수정사항
|
||||
|
||||
### 1. useMemo로 calculatedGrandTotal 추가 (line 194-201)
|
||||
```tsx
|
||||
const calculatedGrandTotal = useMemo(() => {
|
||||
if (!calculationResults?.items) return 0;
|
||||
return calculationResults.items.reduce((sum, itemResult) => {
|
||||
const formItem = formData.items[itemResult.index];
|
||||
return sum + (itemResult.result.grand_total * (formItem?.quantity || 1));
|
||||
}, 0);
|
||||
}, [calculationResults, formData.items]);
|
||||
```
|
||||
|
||||
### 2. Toast 메시지 수정 (line 493-498)
|
||||
`updatedItems` 사용하여 최신 상태 반영
|
||||
|
||||
### 3. Badge 및 하단 총합계
|
||||
`calculatedGrandTotal` 사용 (line 1019, 1131)
|
||||
|
||||
## ⚠️ 남은 이슈
|
||||
사용자가 "견적 산출 결과에는 왜 반영이 안되는거지?"라고 질문 → 확인 필요:
|
||||
1. 수정된 코드로 테스트했는지 (브라우저 새로고침)
|
||||
2. 수량 변경 후 즉시 반영되는지 vs 버튼 클릭 필요한지
|
||||
3. 현재 코드에서 수량 변경 시 합계는 실시간 업데이트되어야 함 (useMemo + React 재렌더링)
|
||||
|
||||
## 📁 관련 파일
|
||||
- `src/components/quotes/QuoteRegistration.tsx` - 메인 수정 파일
|
||||
- `src/components/molecules/FormField.tsx` - FormField 컴포넌트 (참조)
|
||||
- `src/app/[locale]/(protected)/sales/quote-management/[id]/edit/page.tsx` - 수정 페이지
|
||||
|
||||
## 🔑 핵심 교훈
|
||||
**FormField에 커스텀 children(Input, Select, datalist 등)을 넣을 때는 반드시 `type="custom"` 필요!**
|
||||
|
||||
## 🚀 새 세션에서 이어서 작업하려면
|
||||
1. 프로젝트 활성화: Serena `activate_project` → "react"
|
||||
2. 메모리 읽기: `read_memory("quote-registration-formfield-fix.md")`
|
||||
3. 확인 필요: 수량 변경 시 견적 산출 결과가 실시간으로 업데이트되는지 테스트
|
||||
70
.serena/memories/receivables-dynamic-months-fix.md
Normal file
70
.serena/memories/receivables-dynamic-months-fix.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# 채권현황 동적월 지원 및 year=0 파라미터 버그 수정
|
||||
|
||||
## 작업 일시
|
||||
2026-01-02
|
||||
|
||||
## 문제 상황
|
||||
"최근 1년" 필터가 제대로 동작하지 않는 3가지 버그:
|
||||
1. 2026년 조회 후 "최근 1년" 선택 시 2026년 기준 데이터 표시
|
||||
2. 2025년 조회 후 "최근 1년" 선택 시 2025년 기준 데이터 표시
|
||||
3. 초기 페이지 로드 시 "최근 1년" 기본값인데 데이터 없음
|
||||
|
||||
## 원인 분석
|
||||
|
||||
### 프론트엔드 (이전 세션에서 수정됨)
|
||||
- JavaScript에서 `year === 0` 체크가 falsy 값 문제로 제대로 동작하지 않음
|
||||
- `if (year)` 같은 조건문에서 0이 false로 처리됨
|
||||
|
||||
### 백엔드 (이번 세션에서 수정)
|
||||
- Laravel의 `'nullable|boolean'` 검증이 쿼리 파라미터로 전달된 문자열 "true"를 거부
|
||||
- HTTP 쿼리 파라미터는 항상 문자열로 전달됨
|
||||
|
||||
## 수정 내용
|
||||
|
||||
### 1. ReceivablesController.php
|
||||
```php
|
||||
// 변경 전
|
||||
'recent_year' => 'nullable|boolean',
|
||||
|
||||
// 변경 후
|
||||
'recent_year' => 'nullable|string|in:true,false,1,0',
|
||||
|
||||
// 검증 후 boolean 변환
|
||||
if (isset($params['recent_year'])) {
|
||||
$params['recent_year'] = filter_var($params['recent_year'], FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
\Log::info('[Receivables] index params', $params);
|
||||
```
|
||||
|
||||
### 2. actions.ts (이전 세션 수정, 검증됨)
|
||||
```typescript
|
||||
const yearValue = params?.year;
|
||||
if (typeof yearValue === 'number') {
|
||||
if (yearValue === 0) {
|
||||
searchParams.set('recent_year', 'true');
|
||||
} else {
|
||||
searchParams.set('year', String(yearValue));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 핵심 포인트
|
||||
1. **명시적 타입 체크**: `typeof yearValue === 'number'`로 undefined와 0을 구분
|
||||
2. **문자열 boolean 검증**: Laravel에서 `'in:true,false,1,0'` 사용 후 `filter_var()` 변환
|
||||
3. **디버그 로깅**: 개발 중 파라미터 확인을 위한 로그 추가 (테스트 후 제거 필요)
|
||||
|
||||
## Git 커밋
|
||||
- API: `4fa38e3` - feat(API): 채권현황 동적월 지원 및 year=0 파라미터 버그 수정
|
||||
- React: `672b1b4` - feat(WEB): 채권현황 동적월 지원 및 year=0 파라미터 버그 수정
|
||||
- React: `1f32b04` - docs: 채권현황 동적월 지원 작업 현황 업데이트
|
||||
|
||||
## 관련 파일
|
||||
- `/api/app/Http/Controllers/Api/V1/ReceivablesController.php`
|
||||
- `/api/app/Services/ReceivablesService.php`
|
||||
- `/react/src/components/accounting/ReceivablesStatus/actions.ts`
|
||||
- `/react/src/components/accounting/ReceivablesStatus/index.tsx`
|
||||
|
||||
## 후속 작업
|
||||
- [ ] 테스트 완료 후 디버그 로그 제거
|
||||
- [ ] 추가 UI 개선 (사용자 확인 필요)
|
||||
116
.serena/project.yml
Normal file
116
.serena/project.yml
Normal file
@@ -0,0 +1,116 @@
|
||||
# list of languages for which language servers are started; choose from:
|
||||
# al bash clojure cpp csharp csharp_omnisharp
|
||||
# dart elixir elm erlang fortran go
|
||||
# haskell java julia kotlin lua markdown
|
||||
# nix perl php python python_jedi r
|
||||
# rego ruby ruby_solargraph rust scala swift
|
||||
# terraform typescript typescript_vts yaml zig
|
||||
# Note:
|
||||
# - For C, use cpp
|
||||
# - For JavaScript, use typescript
|
||||
# Special requirements:
|
||||
# - csharp: Requires the presence of a .sln file in the project folder.
|
||||
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
||||
# The first language is the default language and the respective language server will be used as a fallback.
|
||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||
languages:
|
||||
- typescript
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: "utf-8"
|
||||
|
||||
# whether to use the project's gitignore file to ignore files
|
||||
# Added on 2025-04-07
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
# list of additional paths to ignore
|
||||
# same syntax as gitignore, so you can use * and **
|
||||
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||
# Added (renamed) on 2025-04-07
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
# the name by which the project can be referenced within Serena
|
||||
project_name: "react"
|
||||
|
||||
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
|
||||
included_optional_tools: []
|
||||
|
||||
# list of mode names to that are always to be included in the set of active modes
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this setting overrides the global configuration.
|
||||
# Set this to [] to disable base modes for this project.
|
||||
# Set this to a list of mode names to always include the respective modes for this project.
|
||||
base_modes:
|
||||
|
||||
# list of mode names that are to be activated by default.
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
||||
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
||||
default_modes:
|
||||
|
||||
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
||||
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
||||
fixed_tools: []
|
||||
|
||||
# override of the corresponding setting in serena_config.yml, see the documentation there.
|
||||
# If null or missing, the value from the global config is used.
|
||||
symbol_info_budget:
|
||||
|
||||
# The language backend to use for this project.
|
||||
# If not set, the global setting from serena_config.yml is used.
|
||||
# Valid values: LSP, JetBrains
|
||||
# Note: the backend is fixed at startup. If a project with a different backend
|
||||
# is activated post-init, an error will be returned.
|
||||
language_backend:
|
||||
515
CLAUDE.md
Normal file
515
CLAUDE.md
Normal file
@@ -0,0 +1,515 @@
|
||||
# SAM ERP 프로젝트 규칙
|
||||
|
||||
SAM 프로젝트(Next.js 프론트엔드) 전용 규칙. 범용 규칙은 `~/.claude/RULES.md` 참조.
|
||||
|
||||
---
|
||||
|
||||
## 프로젝트 개요
|
||||
|
||||
```yaml
|
||||
sam_project:
|
||||
frontend: sam_project/sam-next/sma-next-project/sam-react-prod # Next.js (현재)
|
||||
backend_api: sam_project/sam-api/sam-api # PHP Laravel
|
||||
design: sam_project/sam-design/sam-design # React 디자인 시스템
|
||||
hotfix: sam_project/sam-hotfix/sam-hotfix # E2E 테스트 결과/핫픽스 관리
|
||||
특성: 인증 필수 폐쇄형 ERP 시스템 (SEO 불필요)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Git Workflow
|
||||
**Priority**: 🔴
|
||||
|
||||
### 브랜치 구조
|
||||
| 브랜치 | 역할 | 커밋 상태 |
|
||||
|--------|------|-----------|
|
||||
| `develop` | 평소 작업 브랜치 (자유롭게) | 지저분해도 OK |
|
||||
| `stage` | QA/테스트 환경 | 기능별 squash 정리 |
|
||||
| `main` | 배포용 (기본 브랜치) | 검증된 것만 |
|
||||
| `feature/*` | 큰 기능/실험적 작업 시 | 선택적 사용 |
|
||||
|
||||
### "git 올려줘" 단축 명령어
|
||||
`git 올려줘` 입력 시 **develop에 push**:
|
||||
1. `git status` → 2. `git diff --stat` → 3. `git add -A` → 4. `git commit` (자동 메시지) → 5. `git push origin develop`
|
||||
|
||||
- `snapshot.txt`, `.DS_Store` 파일은 항상 제외
|
||||
- develop에서 자유롭게 커밋 (커밋 메시지 정리 불필요)
|
||||
|
||||
### main에 올리기 (기능별 squash merge) — 필수 규칙
|
||||
사용자가 "main에 올려줘" 또는 특정 기능을 main에 올리라고 지시할 때만 실행.
|
||||
**절대 자동으로 main에 push하지 않음.**
|
||||
|
||||
**🔴 반드시 기능별로 나눠서 올릴 것. 통째로 squash 금지.**
|
||||
|
||||
```bash
|
||||
# 실행 순서
|
||||
git checkout main
|
||||
git pull origin main
|
||||
|
||||
# 1. develop 커밋 이력 분석 → 기능별 그룹 분류
|
||||
git log --oneline main..develop
|
||||
|
||||
# 2. 기능별로 cherry-pick + squash commit (기능 수만큼 반복)
|
||||
git cherry-pick --no-commit <기능A커밋1> <기능A커밋2> ...
|
||||
git commit -m "feat: [기능A 설명]"
|
||||
|
||||
git cherry-pick --no-commit <기능B커밋1> <기능B커밋2> ...
|
||||
git commit -m "feat: [기능B 설명]"
|
||||
|
||||
# 3. push 후 develop으로 복귀
|
||||
git push origin main
|
||||
git checkout develop
|
||||
```
|
||||
|
||||
**기능 분류 기준**:
|
||||
- 같은 도메인/모듈 수정은 하나로 묶기 (예: CEO 대시보드 관련 커밋들)
|
||||
- CI/CD, 문서 등 인프라 변경은 별도 커밋 (예: chore: Jenkinsfile 정비)
|
||||
- 커밋 메시지 타입: feat(기능), fix(버그), refactor(리팩토링), chore(설정/문서)
|
||||
|
||||
**핵심: main에는 기능 단위 커밋만 → 문제 시 `git revert`로 해당 기능만 롤백 가능**
|
||||
|
||||
### feature 브랜치 사용 기준
|
||||
| 상황 | 방법 |
|
||||
|------|------|
|
||||
| 일반 작업 | develop에서 바로 |
|
||||
| 1주일+ 걸리는 큰 기능 | feature/* 따서 작업 |
|
||||
| 실험적 시도 | feature/* 따서 작업 |
|
||||
| 백엔드와 동시 수정 건 | 각자 feature/* 권장 |
|
||||
|
||||
### 금지 사항
|
||||
- ❌ main에 직접 커밋/push
|
||||
- ❌ `git push --force` (main/develop)
|
||||
- ❌ 사용자 지시 없이 main에 merge
|
||||
|
||||
---
|
||||
|
||||
## Client Component 사용 원칙
|
||||
**Priority**: 🔴
|
||||
|
||||
### 배경
|
||||
- 폐쇄형 사이트 → SEO 불필요, 오히려 노출되면 안 됨
|
||||
- Server Component에서는 쿠키 수정(토큰 갱신) 불가
|
||||
|
||||
### 규칙
|
||||
- **Server Component 사용 금지**: `export default async function Page()` 패턴 금지
|
||||
- **Client Component 사용**: 모든 페이지는 `'use client'` 선언 필수
|
||||
- **데이터 로딩**: useEffect에서 Server Action 호출
|
||||
|
||||
```typescript
|
||||
// ✅ 올바른 패턴
|
||||
'use client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getData } from '@/components/.../actions';
|
||||
|
||||
export default function Page() {
|
||||
const [data, setData] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getData()
|
||||
.then(result => setData(result.data))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) return <div>로딩 중...</div>;
|
||||
return <Component initialData={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ 잘못된 패턴
|
||||
export default async function Page() {
|
||||
const result = await getData();
|
||||
return <Component initialData={result.data} />;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HttpOnly Cookie API Communication
|
||||
**Priority**: 🔴
|
||||
|
||||
- HttpOnly 쿠키는 JavaScript로 읽을 수 없음
|
||||
- **모든 인증 API 호출은 Next.js API route 프록시 필수**
|
||||
|
||||
```typescript
|
||||
// ✅ Next.js API Proxy
|
||||
// /src/app/api/proxy/[...path]/route.ts
|
||||
export async function GET(request: NextRequest, { params }: { params: { path: string[] } }) {
|
||||
const token = request.cookies.get('access_token')?.value;
|
||||
const response = await fetch(`${BACKEND_URL}/${params.path.join('/')}`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
// 프론트엔드에서는 프록시 호출
|
||||
const response = await fetch('/api/proxy/item-master/init');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 기획서/스크린샷 기반 UI 구현 프로세스
|
||||
**Priority**: 🔴
|
||||
|
||||
### 기획서 Description 영역 처리
|
||||
기획서 스크린샷의 Description 영역(보통 오른쪽 검은 배경)은 **설명용**이며 UI에 구현하지 않음.
|
||||
빨간 원 번호, 설명 텍스트, 메타 정보 → 절대 UI에 추가 금지.
|
||||
|
||||
### 필수 5단계 프로세스
|
||||
|
||||
**1단계: Description 정독 및 요소 추출**
|
||||
- 각 번호(①②③...) 항목별 정확히 파악
|
||||
- 필터 조건, 테이블 헤더, 버튼/액션, 특수 기능 추출
|
||||
|
||||
**2단계: 구성 계획 작성 및 사용자 확인**
|
||||
🔴 구현 전 반드시 계획 제시 후 사용자 확인 필수. 확인 없이 구현 진행 절대 금지.
|
||||
|
||||
```markdown
|
||||
## [페이지명] 구성 계획
|
||||
### 필터 조건
|
||||
| 필터명 | 타입 | 옵션 | 기본값 |
|
||||
### 테이블 컬럼
|
||||
| 순서 | 컬럼명 | 설명 |
|
||||
### 특수 기능
|
||||
- [기능1]: [설명]
|
||||
```
|
||||
|
||||
**3단계: 기존 패턴 검색**
|
||||
```
|
||||
1순위: 동일 기능 컴포넌트 (예: "*Dashboard*.tsx")
|
||||
2순위: 유사 도메인 컴포넌트
|
||||
3순위: 공통 UI 컴포넌트 (src/components/ui/)
|
||||
```
|
||||
|
||||
**4단계: 구현** - 기획서 요소만, 임의 추가 절대 금지
|
||||
|
||||
**5단계: 검증 체크리스트**
|
||||
```markdown
|
||||
| 기획서 요소 | 구현 여부 | 비고 |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Pattern Reuse
|
||||
**Priority**: 🔴
|
||||
|
||||
- 새 컴포넌트 만들기 전 프로젝트 내 유사 컴포넌트 검색 필수
|
||||
- 스크린샷만으로 추측 금지, 프로젝트 표준 우선
|
||||
|
||||
| 요소 | 확인 사항 |
|
||||
|------|----------|
|
||||
| 모달/다이얼로그 | 너비, 배경색, 헤더 구조, 버튼 배치 |
|
||||
| 문서/프린트 | 용지 스타일, 헤더/푸터, 결재라인 |
|
||||
| 폼 | 레이아웃, 필드 배치, 버튼 위치 |
|
||||
| 테이블/리스트 | 컬럼 구조, 체크박스, 페이지네이션 |
|
||||
|
||||
### 컴포넌트 레지스트리 활용 (dev/component-registry)
|
||||
실시간 스캔 기반 컴포넌트 목록 + 관계도 페이지가 존재함. 새로고침 시 최신 상태 반영.
|
||||
|
||||
**새 컴포넌트 생성 전 필수 확인**:
|
||||
1. **목록 뷰**: 동일/유사 컴포넌트가 이미 있는지 검색
|
||||
2. **관계도 뷰**: 유사 컴포넌트의 구성요소(imports)를 확인하여 동일한 공통 컴포넌트 조합 패턴 따르기
|
||||
|
||||
**기존 컴포넌트 수정 시 필수 확인**:
|
||||
- 관계도의 **사용처(usedBy)** 확인 → 수정 시 영향받는 범위 파악
|
||||
- usedBy가 많은 공통 컴포넌트일수록 수정 시 주의
|
||||
|
||||
---
|
||||
|
||||
## Common Table Standards
|
||||
**Priority**: 🔴
|
||||
|
||||
### 필수 컬럼 구조
|
||||
- **체크박스** → **번호(1부터)** → **데이터 컬럼** → **작업 컬럼**
|
||||
- 작업 버튼: 체크박스 선택 시만 표시
|
||||
- 번호: `globalIndex` 사용 또는 `(currentPage - 1) * pageSize + index + 1`
|
||||
|
||||
---
|
||||
|
||||
## Document Table Merging (rowSpan/colSpan)
|
||||
**Priority**: 🔴
|
||||
|
||||
### 핵심: 구조 분석 → 코딩 (절대 순서 바꾸지 않음)
|
||||
|
||||
**1단계: 플랫 인덱스 맵** - 논리적 No가 아닌 실제 렌더링 행 수 기준
|
||||
```
|
||||
flatIdx 0: No.1 겉모양
|
||||
flatIdx 1: No.2 치수-두께 ← No.2 시작 (methodSpan: 3)
|
||||
flatIdx 2: No.2 치수-너비
|
||||
flatIdx 3: No.2 치수-길이 ← No.2 끝
|
||||
```
|
||||
|
||||
**2단계: 병합 범위 표기** - span은 병합 그룹의 첫 행에만
|
||||
|
||||
**3단계: Coverage Map 패턴**
|
||||
```typescript
|
||||
function buildCoverageMap(items, spanKey) {
|
||||
const map = {}; const covered = new Set();
|
||||
items.forEach((item, idx) => {
|
||||
const span = item[spanKey];
|
||||
if (span && span > 1) {
|
||||
map[idx] = span;
|
||||
for (let i = idx + 1; i < idx + span; i++) covered.add(i);
|
||||
}
|
||||
});
|
||||
return { map, covered };
|
||||
}
|
||||
// map에 있으면 → <td rowSpan={span}>
|
||||
// covered에 있으면 → skip
|
||||
// 둘 다 아니면 → 일반 <td>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Page Layout Standards
|
||||
**Priority**: 🟡
|
||||
|
||||
- **AuthenticatedLayout**: `<main>`에 패딩 없음
|
||||
- **PageLayout**: `p-3 md:p-6` 패딩 담당
|
||||
- **page.tsx**: 패딩 wrapper 금지 (이중 패딩 방지)
|
||||
|
||||
---
|
||||
|
||||
## Design Popup Policy
|
||||
**Priority**: 🟡
|
||||
|
||||
- `alert()`, `confirm()`, `prompt()` 사용 금지
|
||||
- Radix UI Dialog/AlertDialog 또는 `toast from 'sonner'` 사용
|
||||
|
||||
---
|
||||
|
||||
## Radix UI Select Controlled Mode Bug
|
||||
**Priority**: 🟡
|
||||
|
||||
빈 값('')으로 마운트 후 value 변경이 반영 안 되는 버그:
|
||||
```tsx
|
||||
// ✅ key prop으로 강제 리마운트
|
||||
<Select key={`${fieldKey}-${stringValue}`} value={stringValue} onValueChange={onChange}>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build Policy
|
||||
**Priority**: 🔴
|
||||
|
||||
- Claude가 직접 `npm run build` 실행 금지
|
||||
- 빌드 필요 시 사용자에게 "빌드 확인해주세요" 요청
|
||||
|
||||
---
|
||||
|
||||
## React → Next.js Migration Rules
|
||||
**Priority**: 🔴
|
||||
|
||||
### localStorage Access
|
||||
```typescript
|
||||
// ✅ Next.js Pattern
|
||||
const [data, setData] = useState(() => {
|
||||
if (typeof window === 'undefined') return defaultValue;
|
||||
const saved = localStorage.getItem('key');
|
||||
return saved ? JSON.parse(saved) : defaultValue;
|
||||
});
|
||||
```
|
||||
|
||||
### App Router Rules
|
||||
- Client Components: 'use client' for interactivity, state, browser APIs
|
||||
- Dynamic Import: `next/dynamic` with `ssr: false` for client-only components
|
||||
|
||||
---
|
||||
|
||||
## Large File Migration Workflow
|
||||
**Priority**: 🟡
|
||||
|
||||
**섹션당 6단계**: 구조 파악 → 기능 구현 → 기능 검증 → 스타일 파악 → 스타일 구현 → 스타일 검증
|
||||
|
||||
분할 전략: <1000줄 전체 | 1000-3000줄 3-4섹션 | >3000줄 1000줄 단위
|
||||
|
||||
---
|
||||
|
||||
## Backend API Policy
|
||||
**Priority**: 🟡
|
||||
|
||||
- **신규 API 생성 금지**: 새로운 엔드포인트/컨트롤러 생성은 직접 하지 않음 → 요청 문서로 정리
|
||||
- **기존 API 수정/추가 가능**: 이미 존재하는 API의 수정, 필드 추가, 로직 변경은 직접 수행 가능
|
||||
- 백엔드 경로: `sam_project/sam-api/sam-api` (PHP Laravel)
|
||||
- 수정 시 기존 코드 패턴(Service-First, 기존 응답 구조) 준수
|
||||
- 신규 API가 필요한 경우 요청 문서로 정리:
|
||||
```markdown
|
||||
## 백엔드 API 신규 요청
|
||||
### 엔드포인트: [HTTP METHOD /api/v1/path]
|
||||
### 목적: [설명]
|
||||
### 요청/응답 구조: [내용]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test URL Documentation Rules
|
||||
**Priority**: 🟡
|
||||
|
||||
- 메인 페이지만 등록, 세부 페이지(상세/수정/등록) 제외
|
||||
- 간결한 목록 유지
|
||||
|
||||
---
|
||||
|
||||
## Zod 스키마 검증 (신규 코드 적용)
|
||||
**Priority**: 🟡
|
||||
|
||||
### 적용 범위
|
||||
- **신규 폼**: Zod 스키마 필수 적용
|
||||
- **기존 폼**: 건드리지 않음 (정상 작동 중이면 마이그레이션 불필요)
|
||||
- **API 응답**: 신규 서버 액션에서 선택적 적용
|
||||
|
||||
### 신규 폼 작성 패턴
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
// 1. 스키마 정의 (타입 + 검증 한 번에)
|
||||
const formSchema = z.object({
|
||||
itemName: z.string().min(1, '품목명을 입력하세요'),
|
||||
quantity: z.number().min(1, '1 이상 입력하세요'),
|
||||
status: z.enum(['active', 'inactive']),
|
||||
memo: z.string().optional(),
|
||||
});
|
||||
|
||||
// 2. 스키마에서 타입 추출 (별도 interface 정의 불필요)
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
// 3. useForm에 zodResolver 연결
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: { itemName: '', quantity: 1, status: 'active' },
|
||||
});
|
||||
```
|
||||
|
||||
### 규칙
|
||||
- **스키마 위치**: 컴포넌트 파일 상단 또는 같은 디렉토리의 `schema.ts`
|
||||
- **타입 추출**: `z.infer<typeof schema>` 사용, 별도 `interface` 중복 정의 금지
|
||||
- **에러 메시지**: 한글로 작성 (사용자에게 직접 표시됨)
|
||||
- **`as` 캐스트 지양**: Zod 스키마로 타입이 보장되므로 `as` 캐스트 불필요
|
||||
|
||||
### 사용하지 않는 경우
|
||||
- 기존 `rules={{ required: true }}` 패턴으로 작동 중인 폼
|
||||
- 단순 필드 1~2개짜리 인라인 폼 (오버엔지니어링)
|
||||
|
||||
---
|
||||
|
||||
## Server Action 공통 유틸리티
|
||||
**Priority**: 🔴
|
||||
|
||||
### 규칙:
|
||||
- `buildApiUrl()` 사용 필수 (직접 `new URLSearchParams` 또는 `${API_URL}` 조립 금지)
|
||||
- 페이지네이션 조회 → `executePaginatedAction()` 사용
|
||||
- 단건/목록 조회 → `executeServerAction()` 유지
|
||||
- `toPaginationMeta()` 직접 사용도 허용
|
||||
- **`'use server'` 파일에서 타입 re-export 금지** — `export type { X } from '...'` 사용 불가 (Next.js Turbopack 제한: async 함수만 export 허용). 인라인 `export interface` / `export type X = ...`는 허용. 컴포넌트에서 타입이 필요하면 원본 파일에서 직접 import할 것
|
||||
|
||||
### 현황:
|
||||
- **전체 43개 actions.ts 마이그레이션 완료** (2026-02-12)
|
||||
- `new URLSearchParams` 사용 0건 (actions.ts 기준)
|
||||
- 모든 URL 빌딩은 `buildApiUrl(path, params)` 사용
|
||||
|
||||
```typescript
|
||||
// ✅ 필수 패턴
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
|
||||
// 쿼리 파라미터 있는 경우
|
||||
url: buildApiUrl('/api/v1/items', {
|
||||
search: params.search,
|
||||
status: params.status !== 'all' ? params.status : undefined,
|
||||
page: params.page,
|
||||
}),
|
||||
|
||||
// 동적 경로 + 파라미터
|
||||
url: buildApiUrl(`/api/v1/items/${id}`, { with_details: true }),
|
||||
|
||||
// 파라미터 없는 단순 경로
|
||||
url: buildApiUrl('/api/v1/items'),
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ 금지 패턴
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
const params = new URLSearchParams();
|
||||
params.set('search', value);
|
||||
url: `${API_URL}/api/v1/items?${params.toString()}`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Component Usage Rules
|
||||
**Priority**: 🔴
|
||||
|
||||
신규 페이지/모달 작업 시 **반드시** 공통 패턴 가이드를 먼저 읽고 기존 구조를 따를 것.
|
||||
|
||||
**트리거 → 가이드 읽기:**
|
||||
| 작업 유형 | 읽을 파일 |
|
||||
|-----------|----------|
|
||||
| 검색 모달/선택 팝업 | `claudedocs/guides/[GUIDE] common-page-patterns.md` → "검색 모달" 섹션 |
|
||||
| 리스트/목록 페이지 | `claudedocs/guides/[GUIDE] common-page-patterns.md` → "리스트 페이지" 섹션 |
|
||||
| 상세/수정/등록 페이지 | `claudedocs/guides/[GUIDE] common-page-patterns.md` → "상세/폼 페이지" 섹션 |
|
||||
| 새 organisms 필요 | `src/components/organisms/index.ts` 먼저 확인 → 없으면 생성 |
|
||||
|
||||
**핵심 원칙:**
|
||||
- 새 파일 만들기 전 `organisms/`, `molecules/` export 목록 확인
|
||||
- 검색+선택 모달 → `SearchableSelectionModal<T>` 사용 (직접 Dialog 조합 금지)
|
||||
- 리스트 페이지 → `UniversalListPage` 또는 organisms 조합
|
||||
- 상세/폼 → Card + 기존 패턴 따르기
|
||||
|
||||
---
|
||||
|
||||
## FormField 사용 규칙 (신규 폼 필수)
|
||||
**Priority**: 🟡
|
||||
|
||||
### 적용 범위
|
||||
- **신규 폼**: `Label + Input` 수동 조합 대신 `FormField` molecule 필수 사용
|
||||
- **기존 폼**: 건드리지 않음 (해당 파일 수정 시에만 선택적 전환)
|
||||
|
||||
### 사용 패턴
|
||||
```typescript
|
||||
import { FormField } from '@/components/molecules/FormField';
|
||||
|
||||
// ✅ 올바른 패턴 - FormField 사용
|
||||
<FormField
|
||||
label="회사명"
|
||||
value={formData.companyName}
|
||||
onChange={(value) => handleChange('companyName', value)}
|
||||
placeholder="회사명"
|
||||
disabled={!isEditMode}
|
||||
/>
|
||||
|
||||
// ❌ 잘못된 패턴 - Label + Input 수동 조합
|
||||
<div className="space-y-2">
|
||||
<Label>회사명</Label>
|
||||
<Input
|
||||
value={formData.companyName}
|
||||
onChange={(e) => handleChange('companyName', e.target.value)}
|
||||
placeholder="회사명"
|
||||
disabled={!isEditMode}
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
### FormField 지원 타입
|
||||
| type | 설명 | 대체 컴포넌트 |
|
||||
|------|------|---------------|
|
||||
| `text` (기본값) | 일반 텍스트 입력 | Label + Input |
|
||||
| `number` | 숫자 입력 | Label + Input[type=number] |
|
||||
| `email` | 이메일 입력 | Label + Input[type=email] |
|
||||
| `tel` | 전화번호 (자동 포맷) | Label + PhoneInput |
|
||||
| `businessNumber` | 사업자등록번호 (자동 포맷) | Label + BusinessNumberInput |
|
||||
| `textarea` | 여러 줄 텍스트 | Label + Textarea |
|
||||
|
||||
### FormField로 대체하지 않는 경우
|
||||
- **특수 컴포넌트 필드**: Select, DatePicker, ImageUpload, FileInput, AccountNumberInput 등
|
||||
- **복합 레이아웃 필드**: 주소 검색(버튼+입력), 다중 입력 조합
|
||||
- **커스텀 인터랙션**: 편집/읽기 모드가 다른 컴포넌트(예: 결제일 Select↔Input 전환)
|
||||
|
||||
---
|
||||
|
||||
## User Environment
|
||||
**Priority**: 🟢
|
||||
|
||||
- 스크린샷: 항상 바탕화면 `/Users/byeongcheolryu/Desktop/`
|
||||
- 파일명 패턴: `스크린샷 YYYY-MM-DD 오전/오후 HH.MM.SS.png`
|
||||
29
CURRENT_WORKS.md
Normal file
29
CURRENT_WORKS.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# SAM React 작업 현황
|
||||
|
||||
## 🔄 진행 중 - 절곡 작업일지 실 데이터 테스트
|
||||
|
||||
- ✅ 절곡 작업일지 PHP 기준 완전 재구현 (4개 카테고리 섹션)
|
||||
- ✅ 슬랫 작업일지 입고 LOT NO 개소별 표시 수정
|
||||
- ⬜ 절곡 실 데이터 테스트 (bending_info 채워진 작업지시로 확인)
|
||||
|
||||
### 최근 커밋
|
||||
- `59b9b1b` (2026-02-19) feat(WEB): 절곡 작업일지 완전 재구현 + 슬랫 입고 LOT NO 개소별 표시 수정
|
||||
|
||||
---
|
||||
|
||||
## TODO - 미커밋 변경사항
|
||||
|
||||
- `src/components/quotes/types.ts` - `formula_source?: string` 추가 (TypeScript 빌드 에러 수정)
|
||||
|
||||
---
|
||||
|
||||
## TODO - 건설관리 Mock 모듈 (Backend API 개발 후 연동)
|
||||
|
||||
| 모듈 | Backend API | 비고 |
|
||||
|------|-------------|------|
|
||||
| bidding | ❌ 없음 | Backend 필요 |
|
||||
| site-briefings | ❌ 없음 | Backend 필요 |
|
||||
| structure-review | ❌ 없음 | Backend 필요 |
|
||||
| labor-management | ❌ 없음 | Backend 필요 |
|
||||
|
||||
> Mock → API 마이그레이션 진행률: 97% 완료 (41/43 모듈)
|
||||
139
Jenkinsfile
vendored
Normal file
139
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
pipeline {
|
||||
agent any
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
}
|
||||
|
||||
environment {
|
||||
DEPLOY_USER = 'hskwon'
|
||||
RELEASE_ID = new Date().format('yyyyMMdd_HHmmss')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
script {
|
||||
env.GIT_COMMIT_MSG = sh(script: "git log -1 --pretty=format:'%s'", returnStdout: true).trim()
|
||||
}
|
||||
slackSend channel: '#deploy_react', color: '#439FE0', tokenCredentialId: 'slack-token',
|
||||
message: "🚀 *react* 빌드 시작 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
}
|
||||
}
|
||||
|
||||
stage('Prepare Env') {
|
||||
steps {
|
||||
script {
|
||||
if (env.BRANCH_NAME == 'main') {
|
||||
// main: Stage 빌드 먼저 → Production 재빌드
|
||||
sh "cp /var/lib/jenkins/env-files/react/.env.stage .env.production"
|
||||
} else {
|
||||
def envFile = "/var/lib/jenkins/env-files/react/.env.${env.BRANCH_NAME}"
|
||||
sh "cp ${envFile} .env.production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Install') {
|
||||
steps { sh 'npm install --prefer-offline' }
|
||||
}
|
||||
|
||||
stage('Build') {
|
||||
steps { sh 'npm run build' }
|
||||
}
|
||||
|
||||
// ── develop → 개발서버 배포 ──
|
||||
stage('Deploy Development') {
|
||||
when { branch 'develop' }
|
||||
steps {
|
||||
sshagent(credentials: ['deploy-ssh-key']) {
|
||||
sh """
|
||||
rsync -az --delete \
|
||||
--exclude='.git' --exclude='.env*' --exclude='ecosystem.config.*' \
|
||||
.next package.json next.config.ts public node_modules \
|
||||
${DEPLOY_USER}@114.203.209.83:/home/webservice/react/
|
||||
scp .env.production ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/.env.production
|
||||
ssh ${DEPLOY_USER}@114.203.209.83 'cd /home/webservice/react && pm2 restart sam-react'
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── main → 운영서버 Stage 배포 ──
|
||||
stage('Deploy Stage') {
|
||||
when { branch 'main' }
|
||||
steps {
|
||||
sshagent(credentials: ['deploy-ssh-key']) {
|
||||
sh """
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/react-stage/releases/${RELEASE_ID}'
|
||||
rsync -az --delete \
|
||||
.next package.json next.config.ts public node_modules \
|
||||
${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/
|
||||
scp .env.production ${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/.env.production
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 '
|
||||
ln -sfn /home/webservice/react-stage/releases/${RELEASE_ID} /home/webservice/react-stage/current &&
|
||||
cd /home/webservice && pm2 reload sam-front-stage 2>/dev/null || pm2 start react-stage/current/node_modules/.bin/next --name sam-front-stage -- start -p 3100 &&
|
||||
cd /home/webservice/react-stage/releases && ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true
|
||||
'
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 운영 배포 승인 (런칭 후 활성화) ──
|
||||
// stage('Production Approval') {
|
||||
// when { branch 'main' }
|
||||
// steps {
|
||||
// slackSend channel: '#product_deploy', color: '#FF9800', tokenCredentialId: 'slack-token',
|
||||
// message: "🔔 *react* 운영 배포 승인 대기 중\n${env.GIT_COMMIT_MSG}\nStage: https://stage.sam.it.kr\n<${env.BUILD_URL}input|승인하러 가기>"
|
||||
// timeout(time: 24, unit: 'HOURS') {
|
||||
// input message: 'Stage 확인 후 운영 배포를 진행하시겠습니까?\nStage: https://stage.sam.it.kr',
|
||||
// ok: '운영 배포 진행'
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// ── main → Production 재빌드 (운영 환경변수) ──
|
||||
stage('Rebuild for Production') {
|
||||
when { branch 'main' }
|
||||
steps {
|
||||
sh "cp /var/lib/jenkins/env-files/react/.env.main .env.production"
|
||||
sh 'npm run build'
|
||||
}
|
||||
}
|
||||
|
||||
// ── main → 운영서버 Production 배포 ──
|
||||
stage('Deploy Production') {
|
||||
when { branch 'main' }
|
||||
steps {
|
||||
sshagent(credentials: ['deploy-ssh-key']) {
|
||||
sh """
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/react/releases/${RELEASE_ID}'
|
||||
rsync -az --delete \
|
||||
.next package.json next.config.ts public node_modules \
|
||||
${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/
|
||||
scp .env.production ${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/.env.production
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 '
|
||||
ln -sfn /home/webservice/react/releases/${RELEASE_ID} /home/webservice/react/current &&
|
||||
cd /home/webservice && pm2 reload sam-front &&
|
||||
cd /home/webservice/react/releases && ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true
|
||||
'
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
slackSend channel: '#deploy_react', color: 'good', tokenCredentialId: 'slack-token',
|
||||
message: "✅ *react* 배포 성공 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
}
|
||||
failure {
|
||||
slackSend channel: '#deploy_react', color: 'danger', tokenCredentialId: 'slack-token',
|
||||
message: "❌ *react* 배포 실패 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
claudedocs/.DS_Store
vendored
BIN
claudedocs/.DS_Store
vendored
Binary file not shown.
109
claudedocs/[FIX-2026-02-26] windows-compatibility-checklist.md
Normal file
109
claudedocs/[FIX-2026-02-26] windows-compatibility-checklist.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Windows 호환성 개선 계획서
|
||||
|
||||
> 작성일: 2026-02-26
|
||||
> 배경: macOS 개발환경 → Windows 공장 PC 사용자 환경 차이로 인한 이슈
|
||||
> 상태: 계획 단계
|
||||
|
||||
---
|
||||
|
||||
## 완료된 작업
|
||||
|
||||
- [x] Popover 계열 컴포넌트 Windows 포커스 이슈 수정 (6개 파일)
|
||||
- `ui/date-picker.tsx` — onPointerDownOutside/onInteractOutside 방어
|
||||
- `ui/time-picker.tsx` — 동일
|
||||
- `ui/date-range-picker.tsx` — 동일
|
||||
- `ui/multi-select-combobox.tsx` — 동일
|
||||
- `ui/searchable-select.tsx` — 동일
|
||||
- `molecules/ColumnSettingsPopover.tsx` — 동일
|
||||
- [x] 삭제 버튼 아이콘 X → Trash2 통일 (23개 파일)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: IMPORTANT — 사용자 체감 영향 큰 항목
|
||||
|
||||
### 1-1. backdrop-filter 성능 최적화
|
||||
- **파일**: `src/app/globals.css` (line 269-280)
|
||||
- **문제**: `backdrop-filter: blur()` 가 Windows GPU에서 비효율적 → 공장 PC에서 스크롤 버벅거림
|
||||
- **영향 범위**: `.clean-glass` 클래스 사용하는 전체 레이아웃 (Sidebar, Login 등)
|
||||
- **수정 방향**:
|
||||
- `prefers-reduced-motion` / `prefers-reduced-transparency` 미디어쿼리로 분기
|
||||
- 성능 낮은 환경에서는 `backdrop-filter: none` + 불투명 배경 대체
|
||||
- [ ] globals.css `.clean-glass` 수정
|
||||
- [ ] 적용된 컴포넌트에서 시각적 변화 확인
|
||||
|
||||
### 1-2. 폰트 굵기 렌더링 차이 보정
|
||||
- **파일**: `src/app/globals.css` (line 219-235), `src/app/[locale]/layout.tsx` (line 14-19)
|
||||
- **문제**: Windows 폰트 렌더링이 macOS보다 ~0.5-1.5 weight 더 굵게 표시 (Pretendard 변수 폰트)
|
||||
- **영향 범위**: 전체 UI 텍스트
|
||||
- **수정 방향**:
|
||||
- `-webkit-font-smoothing: antialiased` 확인 (이미 적용됨)
|
||||
- `font-weight: 400` 명시적 지정
|
||||
- 필요 시 Windows에서 `font-weight: 350` 적용 검토 (변수 폰트이므로 가능)
|
||||
- [ ] 현재 폰트 설정 확인
|
||||
- [ ] Windows에서 시각 비교 테스트 후 보정값 결정
|
||||
- [ ] globals.css 수정
|
||||
|
||||
### 1-3. 스크롤바 동작 차이
|
||||
- **파일**: `src/app/globals.css` (line 332-412), `src/layouts/AuthenticatedLayout.tsx` (line 173-197)
|
||||
- **문제**: macOS는 스크롤바 자동 숨김, Windows는 항상 표시 → 투명→페이드인 방식이 어색
|
||||
- **영향 범위**: 모든 스크롤 가능한 영역
|
||||
- **수정 방향**:
|
||||
- 스크롤바 thumb을 기본 살짝 보이게 (`rgba(0,0,0,0.1)`)
|
||||
- `.is-scrolling` 시 진하게 (`rgba(0,0,0,0.2)`) 유지
|
||||
- 너비 8px → 10-12px 로 Windows 기대치에 맞게 조정 검토
|
||||
- [ ] globals.css 스크롤바 스타일 수정
|
||||
- [ ] Windows에서 시각 확인
|
||||
|
||||
### 1-4. 숫자 포맷 locale 명시
|
||||
- **파일**: `src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx` (line 65-68)
|
||||
- **문제**: `toLocaleString(undefined, ...)` → Windows 지역 설정에 따라 포맷 달라짐
|
||||
- **영향 범위**: 해당 페이지 숫자 표시 (다른 곳에도 동일 패턴 있는지 추가 검색 필요)
|
||||
- **수정 방향**:
|
||||
- `undefined` → `'ko-KR'` 명시적 locale 지정
|
||||
- 프로젝트 전체에서 동일 패턴 일괄 검색 후 수정
|
||||
- [ ] `toLocaleString(undefined` 패턴 전체 검색
|
||||
- [ ] locale을 `'ko-KR'`로 일괄 변경
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: MINOR — 안정성/효율성 개선
|
||||
|
||||
### 2-1. number input 스피너 버튼 숨김
|
||||
- **파일**: 품질관리 문서 등 `input[type="number"]` 사용처
|
||||
- **문제**: Windows에서 ↑↓ 스피너 버튼 표시 → 터치스크린에서 실수 클릭
|
||||
- **수정 방향**: 전역 CSS로 스피너 숨김 처리
|
||||
- [ ] globals.css에 `input[type="number"]` 스피너 숨김 CSS 추가
|
||||
|
||||
### 2-2. 클립보드 API 에러 핸들링
|
||||
- **파일**: `src/app/[locale]/(protected)/dev/component-registry/ComponentRegistryClient.tsx` (line 44-48)
|
||||
- **문제**: `navigator.clipboard.writeText()` 가 Windows 보안 정책으로 실패 가능
|
||||
- **수정 방향**: try-catch + `document.execCommand('copy')` fallback
|
||||
- [ ] 클립보드 사용 코드 에러 핸들링 추가
|
||||
|
||||
### 2-3. 출퇴근 시계 갱신 주기 최적화
|
||||
- **파일**: `src/app/[locale]/(protected)/hr/attendance/page.tsx` (line ~170-180)
|
||||
- **문제**: `setInterval(1000)` 매초 갱신 → 공장 PC CPU 부하
|
||||
- **수정 방향**: 날짜만 표시하므로 60초 간격으로 변경
|
||||
- [ ] setInterval 주기 1초 → 60초로 변경
|
||||
|
||||
---
|
||||
|
||||
## 테스트 체크리스트
|
||||
|
||||
모든 수정 후 Windows 환경에서 확인:
|
||||
|
||||
- [ ] DatePicker: Dialog 안에서 날짜 선택 → 값 정상 입력
|
||||
- [ ] DatePicker: 이전/다음달 날짜 클릭 → 팝업 유지, 월 이동
|
||||
- [ ] TimePicker: Dialog 안에서 시간 선택 → 정상 동작
|
||||
- [ ] 스크롤: 메인 레이아웃 + 테이블 스크롤 부드러움 확인
|
||||
- [ ] 폰트: 텍스트 두께가 macOS와 비슷한 수준인지 확인
|
||||
- [ ] 숫자 포맷: 천단위 구분자, 소수점 정상 표시
|
||||
- [ ] number input: 스피너 버튼 안 보이는지 확인
|
||||
|
||||
---
|
||||
|
||||
## 참고
|
||||
|
||||
- Windows 공장 PC 사양: 보통 중저사양 (Intel i3-i5, 8GB RAM, 내장 GPU)
|
||||
- 브라우저: Chrome 또는 Edge (Chromium 기반)
|
||||
- 터치스크린 사용 가능성 있음
|
||||
172
claudedocs/[TASK-2026-03-03] daily-report-usd-section.md
Normal file
172
claudedocs/[TASK-2026-03-03] daily-report-usd-section.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# 일일일보 — USD(외국환) 섹션 누락
|
||||
|
||||
**유형**: 프론트엔드 UI 누락
|
||||
**파일**: `src/components/accounting/DailyReport/index.tsx`
|
||||
**날짜**: 2026-03-03
|
||||
|
||||
---
|
||||
|
||||
## 현상
|
||||
|
||||
일일일보 페이지에 KRW(원화) 계좌만 표시되고, USD(외국환) 계좌 섹션이 없음.
|
||||
summary에 `usd_totals`(이월/입금/출금/잔액)이 내려오고, daily-accounts에 `currency: 'USD'` 항목도 내려오지만 UI에서 렌더링하지 않음.
|
||||
|
||||
---
|
||||
|
||||
## 원인
|
||||
|
||||
모든 테이블에서 `currency === 'KRW'` 필터만 적용 중:
|
||||
|
||||
```tsx
|
||||
// line 391 — 계좌별 상세
|
||||
filteredDailyAccounts.filter(item => item.currency === 'KRW')
|
||||
|
||||
// line 448 — 입금 테이블
|
||||
filteredDailyAccounts.filter(item => item.currency === 'KRW' && item.income > 0)
|
||||
|
||||
// line 497 — 출금 테이블
|
||||
filteredDailyAccounts.filter(item => item.currency === 'KRW' && item.expense > 0)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 요구사항
|
||||
|
||||
기존 KRW 섹션과 동일한 구조로 USD 섹션 추가:
|
||||
|
||||
### 1. 일자별 상세 테이블에 USD 행 추가
|
||||
- 기존 KRW 계좌 목록 아래에 USD 계좌 목록 표시
|
||||
- 또는 KRW/USD 구분 소계 행으로 분리
|
||||
- 합계: `accountTotals.usd` 사용 (이미 계산 로직 있음, line 134-144)
|
||||
|
||||
### 2. 예금 입출금 내역에 USD 입금/출금 테이블 추가
|
||||
- 기존 KRW 입금/출금 아래에 USD 입금/출금 테이블 추가
|
||||
- 필터: `currency === 'USD' && item.income > 0` / `currency === 'USD' && item.expense > 0`
|
||||
- 금액 표시: USD 포맷 ($ 또는 달러 표기)
|
||||
|
||||
---
|
||||
|
||||
## 참고: 이미 준비된 데이터
|
||||
|
||||
### summary에서 내려오는 USD 데이터 (line 53-58)
|
||||
```typescript
|
||||
summary: {
|
||||
krwTotals: { carryover, income, expense, balance }, // ← 현재 사용 중
|
||||
usdTotals: { carryover, income, expense, balance }, // ← 미사용 (여기 추가)
|
||||
}
|
||||
```
|
||||
|
||||
### accountTotals 계산 로직 (line 134-144)
|
||||
```typescript
|
||||
// 이미 USD 합계 계산이 있음 — 사용만 하면 됨
|
||||
const usdAccounts = dailyAccounts.filter(item => item.currency === 'USD');
|
||||
const usdTotal = usdAccounts.reduce(
|
||||
(acc, item) => ({
|
||||
carryover: acc.carryover + item.carryover,
|
||||
income: acc.income + item.income,
|
||||
expense: acc.expense + item.expense,
|
||||
balance: acc.balance + item.balance,
|
||||
}),
|
||||
{ carryover: 0, income: 0, expense: 0, balance: 0 }
|
||||
);
|
||||
// accountTotals.usd 로 접근 가능
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 작업 범위
|
||||
|
||||
| 작업 | 설명 |
|
||||
|------|------|
|
||||
| 일자별 상세 테이블 | USD 계좌 행 추가 + USD 소계 행 |
|
||||
| 입금 테이블 | USD 입금 내역 추가 |
|
||||
| 출금 테이블 | USD 출금 내역 추가 |
|
||||
| 금액 포맷 | USD 표시 (달러 기호 또는 통화 표기) |
|
||||
|
||||
**수정 파일**: `src/components/accounting/DailyReport/index.tsx` (이 파일만)
|
||||
**새 코드 불필요**: API 데이터, 타입, 계산 로직 모두 이미 있음. 렌더링만 추가.
|
||||
|
||||
**상태**: ✅ 완료 (프론트엔드 렌더링 추가됨)
|
||||
|
||||
---
|
||||
|
||||
# CEO 대시보드 — 자금현황 데이터 정합성 이슈
|
||||
|
||||
**유형**: 백엔드 데이터 불일치
|
||||
**관련 API**: `GET /api/proxy/daily-report/summary`
|
||||
**관련 파일**: `sam-api/app/Services/DailyReportService.php`
|
||||
**날짜**: 2026-03-03
|
||||
|
||||
---
|
||||
|
||||
## 현상
|
||||
|
||||
CEO 대시보드 자금현황 섹션의 **입금 합계**가 입금 관리 페이지(`/accounting/deposits`)의 실제 데이터와 불일치.
|
||||
|
||||
| 항목 | 대시보드 summary API | 입금 관리 페이지 API | 차이 |
|
||||
|------|---------------------|---------------------|------|
|
||||
| 3월 입금 합계 | **200,000원** | **50,000원** (1건) | **150,000원 차이** |
|
||||
| 3월 출금 합계 | 50,000원 | 50,000원 (1건) | 일치 |
|
||||
|
||||
---
|
||||
|
||||
## 자금현황 각 수치의 의미 (현재 구조)
|
||||
|
||||
```
|
||||
현금성 자산 합계 (cash_asset_total)
|
||||
= KRW 활성 계좌들의 누적 잔액 합계 (당월만이 아닌 전체 잔고)
|
||||
├── 전월이월(carryover): 49,872,638원 ← 3월 이전 누적 (입금총액 - 출금총액)
|
||||
├── 당월입금(income): 200,000원 ← 3월 1일~오늘 입금
|
||||
├── 당월출금(expense): 50,000원 ← 3월 1일~오늘 출금
|
||||
└── 잔액(balance): 50,022,638원 = 이월+입금-출금
|
||||
|
||||
외국환(USD) 합계 (foreign_currency_total) = USD 계좌 잔액 합계
|
||||
입금 합계 = krw_totals.income (당월 KRW 입금만)
|
||||
출금 합계 = krw_totals.expense (당월 KRW 출금만)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 원인 분석
|
||||
|
||||
### 대시보드 summary API 쿼리 (DailyReportService.php line 77-80)
|
||||
```php
|
||||
$income = Deposit::where('tenant_id', $tenantId)
|
||||
->where('bank_account_id', $account->id)
|
||||
->whereBetween('deposit_date', [$startOfMonth, $endOfDay])
|
||||
->sum('amount');
|
||||
```
|
||||
|
||||
### 입금 관리 페이지 API 쿼리
|
||||
- 별도 컨트롤러/서비스에서 조회
|
||||
- 동일한 `deposits` 테이블을 읽지만, 조회 조건이 다를 수 있음
|
||||
|
||||
### 불일치 가능 원인
|
||||
1. **soft delete 차이**: summary는 soft-deleted 레코드 포함, 목록 API는 제외
|
||||
2. **tenant_id 조건 차이**: 두 API의 tenant 필터링이 다를 수 있음
|
||||
3. **E2E 테스트 데이터**: 테스트가 DB에 직접 삽입한 레코드가 목록 API에서는 필터됨
|
||||
4. **status 필터**: 입금 관리 목록에 status 조건이 추가되어 일부 제외
|
||||
|
||||
---
|
||||
|
||||
## 확인 필요 사항 (백엔드)
|
||||
|
||||
### 1. deposits 테이블 직접 조회
|
||||
```sql
|
||||
SELECT id, deposit_date, amount, bank_account_id, deleted_at, status
|
||||
FROM deposits
|
||||
WHERE tenant_id = [현재테넌트]
|
||||
AND bank_account_id = 1
|
||||
AND deposit_date BETWEEN '2026-03-01' AND '2026-03-03'
|
||||
ORDER BY id;
|
||||
```
|
||||
→ 실제 레코드 수와 합계 확인 (soft delete, status 포함)
|
||||
|
||||
### 2. 두 API의 쿼리 조건 비교
|
||||
- `DailyReportService::dailyAccounts()` — Deposit 모델 조건
|
||||
- 입금 관리 컨트롤러/서비스 — Deposit 모델 조건
|
||||
- 차이점 확인 (withTrashed, status 등)
|
||||
|
||||
### 3. 해결 방향
|
||||
- 두 API가 동일한 데이터 소스를 보도록 통일
|
||||
- 또는 대시보드에서 기존 입금/출금 관리 API를 재사용하여 데이터 일관성 확보
|
||||
@@ -1,12 +1,14 @@
|
||||
# claudedocs 문서 맵
|
||||
|
||||
> 프로젝트 기술 문서 인덱스 (Last Updated: 2025-12-24)
|
||||
> 프로젝트 기술 문서 인덱스 (Last Updated: 2026-02-23)
|
||||
|
||||
## ⭐ 빠른 참조
|
||||
## 빠른 참조
|
||||
|
||||
| 문서 | 설명 |
|
||||
|------|------|
|
||||
| **[`[REF] all-pages-test-urls.md`](./[REF]%20all-pages-test-urls.md)** | 🔗 **전체 페이지 테스트 URL 목록** - 모든 페이지 직접 접근 주소 |
|
||||
| **[`[REF] all-pages-test-urls.md`](./dev/[REF]%20all-pages-test-urls.md)** | 전체 페이지 테스트 URL 목록 |
|
||||
| **[`[REF] technical-decisions.md`](./architecture/[REF]%20technical-decisions.md)** | 프로젝트 기술 결정 사항 (13개 항목) |
|
||||
| **[`[GUIDE] common-page-patterns.md`](./guides/[GUIDE]%20common-page-patterns.md)** | 공통 페이지 패턴 가이드 |
|
||||
|
||||
---
|
||||
|
||||
@@ -14,189 +16,40 @@
|
||||
|
||||
```
|
||||
claudedocs/
|
||||
├── _index.md # 이 파일 - 문서 맵
|
||||
├── auth/ # 🔐 인증 & 토큰 관리
|
||||
├── hr/ # 👥 인사관리 (부서/사원)
|
||||
├── item-master/ # 📦 품목기준관리
|
||||
├── sales/ # 💰 판매관리 (견적/거래처)
|
||||
├── accounting/ # 💳 회계관리 (매입/매출/출금)
|
||||
├── board/ # 📝 게시판 관리
|
||||
├── settings/ # ⚙️ 설정 관리 (NEW)
|
||||
├── dashboard/ # 📊 대시보드 & 사이드바
|
||||
├── api/ # 🔌 API 통합
|
||||
├── guides/ # 📚 범용 가이드
|
||||
├── architecture/ # 🏗️ 아키텍처 & 시스템
|
||||
└── archive/ # 📁 레거시/완료된 문서
|
||||
├── _index.md # 이 파일 - 문서 맵
|
||||
├── auth/ # 인증 & 토큰 관리
|
||||
├── hr/ # 인사관리 (부서/사원)
|
||||
├── item-master/ # 품목기준관리
|
||||
├── production/ # 생산관리 (생산현황판/작업자화면)
|
||||
├── quality/ # 품질관리 (검사관리)
|
||||
├── sales/ # 판매관리 (견적/거래처/단가)
|
||||
├── accounting/ # 회계관리 (매입/매출/출금)
|
||||
├── construction/ # 주일 공사 MES
|
||||
├── board/ # 게시판 관리
|
||||
├── settings/ # 설정 관리
|
||||
├── dashboard/ # 대시보드 & 사이드바
|
||||
├── security/ # 보안 & 권한
|
||||
├── api/ # API 통합
|
||||
├── dev/ # 개발도구 & 테스트
|
||||
├── guides/ # 범용 가이드
|
||||
│ ├── mobile/ # 모바일 반응형
|
||||
│ ├── universal-list/ # UniversalListPage 관련
|
||||
│ └── migration/ # 마이그레이션 체크리스트
|
||||
├── architecture/ # 아키텍처 & 시스템 & 기술 결정
|
||||
├── changes/ # 변경이력
|
||||
├── refactoring/ # 리팩토링 체크리스트
|
||||
├── vehicle/ # 차량관리
|
||||
├── material/ # 자재관리
|
||||
├── approval/ # 결재관리
|
||||
├── customer-center/ # 고객센터
|
||||
├── components/ # 컴포넌트 문서
|
||||
├── vercel/ # Vercel 배포
|
||||
└── archive/ # 레거시/완료된 문서
|
||||
└── sessions/ # 만료된 세션 체크포인트
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 auth/ - 인증 & 토큰 관리
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `[IMPL-2025-12-04] signup-page-blocking.md` | ✅ **완료** - MVP 회원가입 페이지 차단 (운영 페이지 이동 예정) |
|
||||
| `token-management-guide.md` | ⭐ **핵심** - Access/Refresh Token 완전 가이드 |
|
||||
| `jwt-cookie-authentication-final.md` | JWT + HttpOnly Cookie 구현 |
|
||||
| `auth-guard-usage.md` | AuthGuard 훅 사용법 |
|
||||
| `route-protection-architecture.md` | 라우트 보호 아키텍처 |
|
||||
| `middleware-issue-resolution.md` | 미들웨어 이슈 해결 |
|
||||
| `safari-cookie-compatibility.md` | Safari 쿠키 호환성 |
|
||||
| `httponly-cookie-implementation.md` | HttpOnly 쿠키 구현 계획 |
|
||||
| `httponly-cookie-security-validation.md` | 보안 검증 케이스 |
|
||||
| `session-migration-*.md` | 세션 마이그레이션 관련 |
|
||||
| `nextjs15-middleware-*.md` | Next.js 15 미들웨어 연구 |
|
||||
|
||||
---
|
||||
|
||||
## 👥 hr/ - 인사관리 (부서/사원/근태/휴가)
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `[IMPL-2025-12-16] mobile-attendance.md` | 🔴 **NEW** - 모바일 출퇴근 시스템 (카카오맵 GPS 기반, MVP) |
|
||||
| `[IMPL-2025-12-05] department-management-checklist.md` | ✅ **완료** - 부서관리 구현 체크리스트 (무제한 트리구조) |
|
||||
| `[IMPL-2025-12-05] employee-management-checklist.md` | ✅ **완료** - 사원관리 구현 체크리스트 |
|
||||
| `[IMPL-2025-12-06] vacation-management-checklist.md` | ✅ **완료** - 휴가관리 구현 체크리스트 |
|
||||
|
||||
---
|
||||
|
||||
## 📦 item-master/ - 품목기준관리
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `[NEXT-2025-12-24] item-master-refactoring-session.md` | ⭐ **세션 체크포인트** - 훅 분리 Phase 1,2 완료, 커밋 대기 |
|
||||
| `[PLAN-2025-12-24] hook-extraction-plan.md` | 🔴 **진행중** - ItemMasterDataManagement 훅 분리 계획서 (1,799줄 → 목표 ~500줄) |
|
||||
| `[IMPL-2025-12-24] item-master-test-and-zustand.md` | 🔴 **진행중** - 훅 분리 테스트 및 Zustand 도입 체크리스트 |
|
||||
| `[PLAN-2025-12-16] dynamicitemform-hook-extraction.md` | ✅ **완료** - DynamicItemForm 훅 분리 계획서 (2161줄 → 1050줄, 51% 감소) |
|
||||
| `[FIX-2025-12-16] options-details-duplicate-bug.md` | options vs item_details 중복 저장 버그 (bending_details 값 덮어쓰기 문제 해결) |
|
||||
| `[IMPL-2025-12-15] backend-item-api-migration.md` | 백엔드 품목 API 통합 (product/material → items), group_id 파라미터, **향후 동적 변경 예정** |
|
||||
| `[NEXT-2025-12-13] item-file-upload-session-context.md` | ⭐ **세션 체크포인트** - 파일 업로드 UI 개선 완료, 백엔드 대기 중, DynamicItemForm 분리 예정 |
|
||||
| `[NEXT-2025-12-12] item-crud-session-context.md` | 📁 이전 세션 - BOM/파일 연동 완료, 파일 업로드 동적화 작업 추가 |
|
||||
| `[DESIGN-2025-12-12] item-master-form-builder-roadmap.md` | 🆕 **로드맵** - Low-Code Form Builder 확장 설계 (노션 스타일 블록 시스템) |
|
||||
| `[PLAN-2025-12-08] dynamic-form-separation-plan.md` | 📋 DynamicItemForm 품목별 분리 계획 (Phase 2: 컴포넌트 구조 설계) |
|
||||
| `[REF] item-code-hardcoding.md` | ⭐ **핵심** - 품목관리 하드코딩 내역 종합 (품목유형/코드자동생성/전개도/BOM) |
|
||||
| `[IMPL-2025-12-02] item-code-auto-generation.md` | 품목코드 자동생성 구현 상세 |
|
||||
| `[PLAN-2025-12-01] service-layer-refactoring.md` | ✅ **완료** - 서비스 레이어 리팩토링 계획 (도메인 로직 중앙화) |
|
||||
| `[REF-2025-12-01] state-sync-solutions.md` | 📋 **참조** - 상태 동기화 문제 및 해결 방안 (정규화, React Query 등) |
|
||||
| `[PLAN-2025-11-28] dynamic-item-form-implementation.md` | ⚠️ **롤백됨** - 이전 구현 계획 (참조용) |
|
||||
| `[IMPL-2025-12-02] dynamic-item-form-rebuild.md` | 🔄 **진행중** - 품목관리 동적 페이지 재구현 (디자인 100% 동일 유지) |
|
||||
| `[API-REQUEST-2025-11-28] dynamic-page-rendering-api.md` | ⭐ **v3.1** - 동적 페이지 렌더링 API 요청서 (ID 기반 통일) |
|
||||
| `[PLAN-2025-11-27] item-form-component-separation.md` | ✅ **완료** - ItemForm 컴포넌트 분리 (1607→415줄, 74% 감소) |
|
||||
| `[IMPL-2025-11-27] realtime-sync-fixes.md` | 실시간 동기화 수정 (BOM, 섹션 복제, 항목 수정, **페이지 삭제 시 섹션 동기화** 2025-11-28) |
|
||||
| `item-master-api-pending-tasks.md` | 진행중인 API 연동 작업 |
|
||||
| `item-master-pending-integration.md` | 대기중인 통합 작업 |
|
||||
| `item-master-specification.md` | API 명세 |
|
||||
| `item-master-backend-requirements.md` | 백엔드 요구사항 |
|
||||
| `item-management-dynamic-api-spec.md` | 동적 필드 API 스펙 |
|
||||
| `item-management-dynamic-frontend.md` | 동적 필드 프론트엔드 설계 |
|
||||
| `item-master-data-management.md` | 데이터 관리 분석 |
|
||||
| `item-master-hooks-refactoring.md` | Hooks 리팩토링 |
|
||||
| `ITEM-MANAGEMENT-MIGRATION.md` | 마이그레이션 가이드 |
|
||||
|
||||
---
|
||||
|
||||
## 💰 sales/ - 판매관리 (견적/거래처/단가)
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `[API-2025-12-08] pricing-api-enhancement-request.md` | 🔴 **NEW** - 단가관리 백엔드 API 개선 요청서 (스키마 변경, 신규 엔드포인트) |
|
||||
| `[IMPL-2025-12-05] pricing-management-migration.md` | 🔄 **진행중** - 단가관리 마이그레이션 계획 (7 Phase, 체크리스트, 원가/마진 계산 로직) |
|
||||
| `[API-2025-12-04] quote-api-request.md` | ⭐ **NEW** - 견적관리 API 요청서 (데이터 모델, 엔드포인트, 수식 계산) |
|
||||
| `[PLAN-2025-12-04] quote-management-implementation.md` | 📋 **NEW** - 견적관리 작업계획서 (6 Phase, 체크리스트) |
|
||||
| `[NEXT-2025-12-09] client-session-context.md` | ⭐ **세션 체크포인트** - 다음 세션 이어하기용 (완료/숨긴 섹션/다음 작업) |
|
||||
| `[IMPL-2025-12-04] client-management-api-integration.md` | ✅ **완료** - 거래처관리 API 연동 체크리스트 (CRUD, 그룹 훅) |
|
||||
| `[API-2025-12-04] client-api-analysis.md` | ✅ **완료** - 거래처 API 분석 (2차 필드 완료, is_active Boolean) |
|
||||
| `[PLAN-2025-12-02] sales-pages-migration.md` | 📋 견적관리/거래처관리 마이그레이션 계획 |
|
||||
|
||||
---
|
||||
|
||||
## 📊 dashboard/ - 대시보드 & 사이드바
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `dashboard-integration-complete.md` | 대시보드 통합 완료 |
|
||||
| `dashboard-cleanup-summary.md` | 정리 요약 |
|
||||
| `dashboard-migration-summary.md` | 마이그레이션 요약 |
|
||||
| `sidebar-active-menu-sync.md` | 사이드바 메뉴 동기화 |
|
||||
| `sidebar-scroll-improvements.md` | 스크롤 개선 |
|
||||
|
||||
---
|
||||
|
||||
## 🔌 api/ - API 통합
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `api-requirements.md` | API 요구사항 |
|
||||
| `api-analysis.md` | API 분석 |
|
||||
| `api-route-type-safety.md` | 라우트 타입 안전성 |
|
||||
| `api-key-management.md` | API 키 관리 |
|
||||
|
||||
---
|
||||
|
||||
## 📚 guides/ - 범용 가이드
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `[PLAN-2025-12-19] project-health-improvement.md` | ✅ **Phase 1 완료** - 프로젝트 헬스 개선 계획서 (타입에러 0개, API키 보안, SSR 수정) |
|
||||
| `[PLAN-2025-12-19] page-layout-standardization.md` | 🔴 **NEW** - 페이지 레이아웃 표준화 계획 |
|
||||
| `[GUIDE-2025-12-16] options-vs-flattened-data.md` | options vs 평탄화 데이터 패턴 (API 응답 매핑 시 options 직접 파싱 금지) |
|
||||
| `[GUIDE] large-file-handling-strategy.md` | 대용량 파일 처리 전략 (100MB+ CAD 도면, 청크 업로드, 스트리밍 다운로드) |
|
||||
| `[FIX-2025-12-05] radix-ui-select-controlled-mode-bug.md` | ⭐ **핵심** - Radix UI Select 버그 해결 (Edit 모드 값 표시 안됨 → key prop 강제 리마운트) |
|
||||
| `i18n-usage-guide.md` | 다국어 사용 가이드 |
|
||||
| `form-validation-guide.md` | 폼 유효성 검사 |
|
||||
| `CSS-MIGRATION-WORKFLOW.md` | CSS 마이그레이션 워크플로우 |
|
||||
| `LARGE-FILE-WORKFLOW.md` | 대용량 파일 작업 워크플로우 |
|
||||
| `ZOD-VALIDATION-TROUBLESHOOTING.md` | Zod 유효성 검사 트러블슈팅 |
|
||||
| `nextjs-error-handling-guide.md` | Next.js 에러 처리 |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ architecture/ - 아키텍처 & 시스템
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `[DESIGN-2025-12-20] item-master-zustand-refactoring.md` | 🔴 **핵심** - 품목기준관리 Zustand 리팩토링 설계서 (3방향 동기화 → 정규화 상태, 테스트 페이지 전략) |
|
||||
| `[NEXT-2025-12-20] zustand-refactoring-session-context.md` | ⭐ **세션 체크포인트** - Phase 1 시작 전, 다음 세션 이어하기용 |
|
||||
| `multi-tenancy-implementation.md` | 멀티테넌시 구현 |
|
||||
| `multi-tenancy-test-guide.md` | 멀티테넌시 테스트 |
|
||||
| `architecture-integration-risks.md` | 통합 리스크 |
|
||||
| `browser-support-policy.md` | 브라우저 지원 정책 |
|
||||
| `ssr-hydration-fix.md` | SSR 하이드레이션 수정 |
|
||||
|
||||
---
|
||||
|
||||
## 💳 accounting/ - 회계관리 (거래처/매입/매출/출금)
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `[IMPL-2025-12-18] vendor-management-checklist.md` | 🔴 **NEW** - 거래처관리 구현 체크리스트 (리스트 + 상세 페이지) |
|
||||
| `[IMPL-2025-12-18] purchase-management.md` | 매입관리 페이지 구현 (리스트 + 상세 모달) |
|
||||
|
||||
---
|
||||
|
||||
## 📝 board/ - 게시판 관리
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `[PLAN-2025-12-19] board-management-implementation.md` | 🔴 **NEW** - 게시판 구현 계획서 (리스트/등록/상세/댓글, TipTap 에디터) |
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ settings/ - 설정 관리
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `[IMPL-2025-12-19] company-info.md` | 🔴 **NEW** - 회사정보 구현 (폼 기반, 회사 추가 팝업) |
|
||||
| `[IMPL-2025-12-19] popup-management.md` | 팝업관리 구현 (리스트/등록/상세/수정, RichTextEditor) |
|
||||
|
||||
---
|
||||
|
||||
## 📁 archive/ - 레거시/완료된 문서
|
||||
|
||||
완료되거나 더 이상 활성화되지 않은 문서들. 참조용으로 보관.
|
||||
|
||||
---
|
||||
|
||||
## 문서 작성 규칙
|
||||
|
||||
### 파일명 컨벤션
|
||||
@@ -213,15 +66,25 @@ claudedocs/
|
||||
- `PLAN` - 계획 문서
|
||||
- `DESIGN` - 설계 문서
|
||||
- `TEST` - 테스트 가이드
|
||||
- `NEXT` - 다음 작업 목록
|
||||
- `NEXT` - 다음 작업 목록 (세션 체크포인트)
|
||||
- `FIX` - 버그 해결 문서
|
||||
- `QA` - 품질 검사 문서
|
||||
- `HOTFIX` - 긴급 수정 문서
|
||||
- `REPORT` - 보고서/전달 문서
|
||||
|
||||
### 폴더 배치 기준
|
||||
1. **기능/도메인 우선**: 문서 주제에 맞는 폴더에 배치
|
||||
2. **범용 가이드**: 여러 기능에 적용되면 `guides/`에 배치
|
||||
3. **완료된 작업**: 더 이상 활성화되지 않으면 `archive/`로 이동
|
||||
4. **신규 도메인**: 3개 이상 문서가 생기면 새 폴더 생성 고려
|
||||
3. **시스템 전체**: 아키텍처/리팩토링/기술결정은 `architecture/`에 배치
|
||||
4. **개발도구**: 테스트 URL, 빌드, E2E, 설정은 `dev/`에 배치
|
||||
5. **완료된 작업**: 더 이상 활성화되지 않으면 `archive/`로 이동
|
||||
6. **만료 세션**: 2개월 이상 경과한 NEXT-* 파일은 `archive/sessions/`로 이동
|
||||
|
||||
### 문서 업데이트
|
||||
- 중요 변경 시 문서 상단에 날짜와 함께 변경사항 기록
|
||||
- `_index.md`에 새 문서 추가 시 테이블 업데이트
|
||||
### 파일 목록 확인
|
||||
```bash
|
||||
# 특정 도메인 파일 확인
|
||||
ls claudedocs/<domain>/
|
||||
|
||||
# 전체 파일 검색
|
||||
find claudedocs/ -name "*.md" | sort
|
||||
```
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# 신규 거래처 신용분석 모달
|
||||
|
||||
## 개요
|
||||
- **목적**: 신규 거래처 등록 시 국가관리 API를 통해 받아온 기업 신용정보를 표시
|
||||
- **위치**: 거래처 등록 완료 후 모달로 표시
|
||||
- **현재 단계**: 목업 데이터로 UI 구현 (추후 API 연동)
|
||||
|
||||
## 화면 구성
|
||||
|
||||
### 1. 헤더
|
||||
- 로고 + "SAM 기업 신용분석 리포트"
|
||||
- 조회일시 표시
|
||||
|
||||
### 2. 기업 정보
|
||||
- "신규거래 신용정보 조회" 뱃지
|
||||
- "기업 신용 분석" 제목
|
||||
- 사업자번호, 법인명 (대표자명), 평가기준일 정보
|
||||
|
||||
### 3. 자료 효력기간 안내
|
||||
- 노란 배경의 알림 박스
|
||||
- 데이터 유효기간 및 면책 안내
|
||||
|
||||
### 4. 종합 신용 신호등
|
||||
- 5단계 신호등 표시 (Level 1~5)
|
||||
- 현재 레벨 강조 (예: 양호 Level 4)
|
||||
- 신용 등급 설명 텍스트
|
||||
- "유료 상세 분석 제공받기" 버튼
|
||||
|
||||
### 5. 신용 리스크 프로필
|
||||
- 오각형 레이더 차트
|
||||
- 한국신용평가등급
|
||||
- 금융 종합 위험도
|
||||
- 매입 결제
|
||||
- 매출 결제
|
||||
- 저당권설정
|
||||
|
||||
### 6. 신용 상세 정보
|
||||
- 신용채무정보 버튼
|
||||
- 신용등급추이정보 버튼
|
||||
- 정보 없음 안내 텍스트
|
||||
|
||||
### 7. 하단 거래 승인 판정
|
||||
- 안전/위험 배지
|
||||
- 신용등급 (Level 1~5)
|
||||
- 거래 유형 (계속사업자/신규거래 등)
|
||||
- 외상 가능 여부
|
||||
- "거래 승인 완료" 버튼
|
||||
|
||||
## 데이터 구조
|
||||
|
||||
```typescript
|
||||
interface CreditAnalysisData {
|
||||
// 기업 정보
|
||||
businessNumber: string; // 사업자번호
|
||||
companyName: string; // 법인명
|
||||
representativeName: string; // 대표자명
|
||||
evaluationDate: string; // 평가기준일
|
||||
|
||||
// 신용 등급
|
||||
creditLevel: 1 | 2 | 3 | 4 | 5; // 1: 위험, 5: 최우량
|
||||
creditStatus: '위험' | '주의' | '보통' | '양호' | '우량';
|
||||
|
||||
// 리스크 프로필 (0~100)
|
||||
riskProfile: {
|
||||
koreaCreditRating: number; // 한국신용평가등급
|
||||
financialRisk: number; // 금융 종합 위험도
|
||||
purchasePayment: number; // 매입 결제
|
||||
salesPayment: number; // 매출 결제
|
||||
mortgageSetting: number; // 저당권설정
|
||||
};
|
||||
|
||||
// 거래 승인 판정
|
||||
approval: {
|
||||
safety: '안전' | '주의' | '위험';
|
||||
level: number;
|
||||
businessType: string; // 계속사업자, 신규거래 등
|
||||
creditAvailable: boolean; // 외상 가능 여부
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 파일 구조
|
||||
|
||||
```
|
||||
src/components/accounting/VendorManagement/
|
||||
├── CreditAnalysisModal.tsx # 신용분석 모달 컴포넌트
|
||||
└── CreditAnalysisModal/
|
||||
├── index.tsx # 메인 모달
|
||||
├── CreditSignal.tsx # 신용 신호등 컴포넌트
|
||||
├── RiskRadarChart.tsx # 레이더 차트 컴포넌트
|
||||
└── types.ts # 타입 정의
|
||||
|
||||
src/app/[locale]/(protected)/dev/
|
||||
└── credit-analysis-test/
|
||||
└── page.tsx # 테스트 페이지
|
||||
```
|
||||
|
||||
## 구현 순서
|
||||
|
||||
1. [x] 계획 md 파일 작성
|
||||
2. [ ] CreditAnalysisModal 컴포넌트 생성
|
||||
3. [ ] 테스트 페이지 생성
|
||||
4. [ ] dev/test-urls에 URL 추가
|
||||
@@ -0,0 +1,52 @@
|
||||
# 백엔드 API 수정 요청: 당월 예상 지출 상세 - 날짜 범위 필터링
|
||||
|
||||
## 엔드포인트
|
||||
`GET /api/v1/expected-expenses/dashboard-detail`
|
||||
|
||||
## 현재 상태
|
||||
- `transaction_type` 파라미터만 지원 (purchase, card, bill)
|
||||
- `start_date`, `end_date` 파라미터를 **무시**함
|
||||
- `items` 배열이 항상 **당월(현재 월)** 기준으로만 반환됨
|
||||
- `summary`도 당월 기준 고정 (total_amount, change_rate 등)
|
||||
- `monthly_trend`만 여러 월 데이터 포함 (최근 7개월)
|
||||
|
||||
## 요청 내용
|
||||
|
||||
### 1. 날짜 범위 필터 지원 추가
|
||||
```
|
||||
GET /api/v1/expected-expenses/dashboard-detail?transaction_type=purchase&start_date=2026-01-01&end_date=2026-01-31
|
||||
```
|
||||
|
||||
| 파라미터 | 타입 | 설명 | 기본값 |
|
||||
|---------|------|------|--------|
|
||||
| `start_date` | string (yyyy-MM-dd) | 조회 시작일 | 당월 1일 |
|
||||
| `end_date` | string (yyyy-MM-dd) | 조회 종료일 | 당월 말일 |
|
||||
| `search` | string | 거래처/항목 검색 | (없음) |
|
||||
|
||||
### 2. 기대 동작
|
||||
- `items`: `start_date` ~ `end_date` 범위의 거래 내역만 반환
|
||||
- `summary.total_amount`: 해당 기간의 합계
|
||||
- `summary.change_rate`: 해당 기간 vs 직전 동일 기간 비교
|
||||
- `vendor_distribution`: 해당 기간 기준 분포
|
||||
- `footer_summary`: 해당 기간 기준 합계
|
||||
- `monthly_trend`: 변경 불필요 (기존처럼 최근 7개월 유지)
|
||||
|
||||
### 3. 검색 필터 (선택)
|
||||
- `search` 파라미터로 거래처명/항목명 부분 검색
|
||||
|
||||
## 검증 데이터
|
||||
현재 `monthly_trend` 기준 데이터가 있는 월:
|
||||
- 11월: 14,101,865원
|
||||
- 12월: 35,241,935원
|
||||
- 1월: 3,000,000원
|
||||
- 2월: 1,650,000원
|
||||
|
||||
`start_date=2026-01-01&end_date=2026-01-31` 조회 시:
|
||||
- `items`: 1월 거래 내역 (현재 빈 배열)
|
||||
- `summary.total_amount`: 3,000,000 (현재 0)
|
||||
|
||||
## 프론트엔드 준비 상태
|
||||
- 프록시: 쿼리 파라미터 정상 전달 확인
|
||||
- 훅: `fetchData(cardId, { startDate, endDate, search })` 지원
|
||||
- 모달: 조회 버튼 + 날짜 필터 UI 완료
|
||||
- 백엔드 수정만 되면 즉시 동작
|
||||
@@ -0,0 +1,821 @@
|
||||
# CEO Dashboard 백엔드 API 명세서
|
||||
|
||||
**작성일**: 2026-03-03
|
||||
**기획서**: SAM_ERP_Storyboard_D1.7_260227.pdf p33~60
|
||||
**프론트엔드 타입**: `src/lib/api/dashboard/types.ts`
|
||||
**대상**: 백엔드 팀 (Laravel sam-api)
|
||||
|
||||
---
|
||||
|
||||
## 공통 규칙
|
||||
|
||||
### 응답 형식
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "조회 성공",
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### 인증
|
||||
- 모든 API는 `Authorization: Bearer {access_token}` 필수
|
||||
- Next.js API route 프록시(`/api/proxy/...`) 경유
|
||||
|
||||
### 캐싱
|
||||
- `sam_stat` 테이블 5분 캐시 (기존 구현 유지)
|
||||
- 대시보드 API는 실시간성보다 성능 우선
|
||||
|
||||
### 날짜/기간 파라미터 규칙
|
||||
- 날짜: `YYYY-MM-DD` (예: `2026-03-03`)
|
||||
- 월: `YYYY-MM` (예: `2026-03`)
|
||||
- 분기: `year=2026&quarter=1`
|
||||
- 기본값: 파라미터 미지정 시 **당월/당분기** 기준
|
||||
|
||||
---
|
||||
|
||||
## 검수 중 발견된 누락 API
|
||||
|
||||
### N1. 오늘의 이슈 — 과거 이력 저장 및 조회
|
||||
**우선순위**: 상
|
||||
**페이지**: p34
|
||||
**현상**: `GET /api/v1/today-issues/summary?date=2026-02-17` 호출 시 항상 `{"items":[], "total_count":0}` 반환. 과거 이슈를 저장하는 구조가 없어서 이전 이슈 탭이 항상 빈 목록.
|
||||
|
||||
**요구사항**:
|
||||
1. **이슈 이력 테이블** 필요 (예: `dashboard_issue_history`)
|
||||
- 매일 자정(또는 배치) 시점에 당일 이슈 스냅샷 저장
|
||||
- 또는 이슈 발생 시점에 이력 테이블에 INSERT
|
||||
2. **기존 API 수정**: `GET /api/v1/today-issues/summary`
|
||||
- `date` 파라미터가 있을 때 해당 날짜의 이력 데이터 반환
|
||||
- `date` 파라미터가 없으면 기존대로 실시간 집계
|
||||
|
||||
**Response** (기존 `TodayIssueApiResponse`와 동일):
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "issue-20260302-001",
|
||||
"badge": "수주",
|
||||
"notification_type": "sales_order",
|
||||
"content": "대한건설 수주 3건 접수",
|
||||
"time": "14:30",
|
||||
"date": "2026-03-02",
|
||||
"path": "/ko/sales/order-management",
|
||||
"needs_approval": false
|
||||
}
|
||||
],
|
||||
"total_count": 5
|
||||
}
|
||||
```
|
||||
|
||||
**Laravel 힌트**:
|
||||
- 배치 저장 방식: `App\Console\Commands\SnapshotDailyIssues` (Schedule::daily)
|
||||
- 또는 이벤트 기반: 수주/채권/재고 변동 시 `dashboard_issue_history` INSERT
|
||||
|
||||
### N2. 자금현황 — 전일 대비 변동률 (daily_change)
|
||||
**우선순위**: 중
|
||||
**페이지**: p33
|
||||
**현상**: `GET /api/v1/daily-report/summary` 응답에 `daily_change` 필드가 없음. 프론트엔드에서 하드코딩 fallback 값(+5.2%, +2.1%, +12.0%, -8.0%)을 사용 중.
|
||||
|
||||
**요구사항**:
|
||||
1. **기존 API 수정**: `GET /api/v1/daily-report/summary`
|
||||
2. 응답에 `daily_change` 객체 추가
|
||||
3. 각 항목의 전일 대비 변동률(%) 계산 로직:
|
||||
- `cash_asset_change_rate`: (오늘 현금성자산 - 어제 현금성자산) / 어제 현금성자산 × 100
|
||||
- `foreign_currency_change_rate`: (오늘 외국환 - 어제 외국환) / 어제 외국환 × 100
|
||||
- `income_change_rate`: (오늘 입금 - 어제 입금) / 어제 입금 × 100
|
||||
- `expense_change_rate`: (오늘 지출 - 어제 지출) / 어제 지출 × 100
|
||||
4. 어제 데이터 없을 시 해당 필드 `null` (프론트에서 fallback 처리)
|
||||
|
||||
**Response** (기존 응답에 `daily_change` 추가):
|
||||
```json
|
||||
{
|
||||
"date": "2026-03-03",
|
||||
"day_of_week": "화",
|
||||
"cash_asset_total": 1250000000,
|
||||
"foreign_currency_total": 85000,
|
||||
"krw_totals": { "income": 45000000, "expense": 32000000, "balance": 1250000000 },
|
||||
"daily_change": {
|
||||
"cash_asset_change_rate": 5.2,
|
||||
"foreign_currency_change_rate": 2.1,
|
||||
"income_change_rate": 12.0,
|
||||
"expense_change_rate": -8.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Laravel 힌트**:
|
||||
- `DailyReportService`에서 전일 데이터 조회 추가
|
||||
- `sam_stat` 캐시 테이블에 전일 스냅샷 있으면 활용
|
||||
- 프론트 타입: `DailyChangeRate` (`src/lib/api/dashboard/types.ts:23`)
|
||||
|
||||
### N3. 일일일보 — daily-accounts에 입출금관리 데이터 미반영
|
||||
**우선순위**: 상
|
||||
**페이지**: 일일일보 페이지 (`/ko/accounting/daily-report`)
|
||||
**현상**: 입금관리/출금관리에서 당일 거래를 등록하면 대시보드 자금현황(`daily-report/summary`)의 합계에는 즉시 반영되지만, 일일일보 페이지의 계좌별 상세 테이블(`daily-report/daily-accounts`)에는 표시되지 않음. (출금 테스트로 확인됨, 입금도 동일 구조로 미반영 추정)
|
||||
|
||||
**영향 범위**:
|
||||
| 데이터 | 관리 테이블 | summary (합계) | daily-accounts (상세) |
|
||||
|--------|-----------|:-:|:-:|
|
||||
| 입금 | `deposits` (`/api/v1/deposits`) | ✅ 반영 추정 | ❌ 미반영 추정 |
|
||||
| 출금 | `withdrawals` (`/api/v1/withdrawals`) | ✅ 반영 확인 | ❌ 미반영 확인 |
|
||||
| 외국환 (USD) | 별도 관리 페이지 미확인 | ✅ 반영 | ❓ 확인 필요 |
|
||||
|
||||
**원인 분석**:
|
||||
- `GET /api/v1/daily-report/summary` → `krw_totals`에 `deposits`/`withdrawals` 테이블 데이터 포함 ✅
|
||||
- `GET /api/v1/daily-report/daily-accounts` → `bank_accounts` 단위 집계만 반환, `deposits`/`withdrawals` 테이블 미포함 ❌
|
||||
|
||||
**데이터 흐름**:
|
||||
```
|
||||
입금관리 등록 → deposits 테이블 INSERT (bank_account_id 포함)
|
||||
출금관리 등록 → withdrawals 테이블 INSERT (bank_account_id 포함)
|
||||
├─ summary API → krw_totals.income/expense에 합산 → 대시보드 ✅
|
||||
└─ daily-accounts API → bank_accounts 기준만 조회 → 일일일보 상세 ❌
|
||||
```
|
||||
|
||||
**요구사항**:
|
||||
1. `GET /api/v1/daily-report/daily-accounts` 수정
|
||||
2. 각 계좌별로 `deposits` 테이블의 당일 income과 `withdrawals` 테이블의 당일 expense를 합산
|
||||
3. 또는 입금/출금 등록 시 해당 계좌의 거래 내역(`bank_account_transactions`)에도 자동 반영
|
||||
|
||||
**해결 방안 (택 1)**:
|
||||
- **방안 A** (daily-accounts 쿼리 수정): `bank_accounts` LEFT JOIN `deposits`/`withdrawals` WHERE date = 당일 → 계좌별 income/expense에 합산
|
||||
- **방안 B** (트랜잭션 연동): 입금/출금 등록 시 `bank_account_transactions`에도 INSERT → daily-accounts가 자연스럽게 포함
|
||||
|
||||
**Response** (기존 `DailyAccountItemApi[]`와 동일, 데이터만 보완):
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "acc_1",
|
||||
"category": "우리은행 123-456",
|
||||
"match_status": "matched",
|
||||
"carryover": 50000000,
|
||||
"income": 1000000,
|
||||
"expense": 50000,
|
||||
"balance": 50950000,
|
||||
"currency": "KRW"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Laravel 힌트**:
|
||||
- `DailyReportService`의 `getDailyAccounts()` 메서드 확인
|
||||
- `deposits` 테이블: `deposit.bank_account_id`로 해당 계좌 income 합산
|
||||
- `withdrawals` 테이블: `withdrawal.bank_account_id`로 해당 계좌 expense 합산
|
||||
- USD 계좌도 동일 패턴 적용 필요
|
||||
|
||||
### N4. 현황판 `purchases`(발주) — path 오류 + 데이터 정합성 이슈
|
||||
**우선순위**: 중
|
||||
**페이지**: p34 (현황판)
|
||||
|
||||
#### 이슈 A: path 하드코딩 오류
|
||||
**현상**: `purchases` 항목의 실제 데이터는 `purchases` 테이블(매입, 공통)에서 조회하면서, path는 건설 모듈 경로로 하드코딩되어 있음.
|
||||
|
||||
**문제 코드** (`StatusBoardService.php` — `getPurchaseStatus()`):
|
||||
```php
|
||||
$count = Purchase::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('status', 'draft')
|
||||
->count();
|
||||
|
||||
return [
|
||||
'id' => 'purchases',
|
||||
'label' => '발주',
|
||||
'path' => '/construction/order/order-management', // ← 매입 데이터인데 건설 경로
|
||||
];
|
||||
```
|
||||
|
||||
- 데이터 출처: `purchases` 테이블 (모든 테넌트 공통 매입 테이블)
|
||||
- path: `/construction/order/order-management` (건설 전용 페이지)
|
||||
- **데이터와 path가 불일치** — 매입 draft 건수를 보여주면서 건설 발주 페이지로 링크
|
||||
|
||||
**현재 프론트 임시 대응**: `status-issue.ts`에서 `/accounting/purchase`(매입관리)로 오버라이드 중
|
||||
|
||||
**요구사항**:
|
||||
1. path를 `/accounting/purchase`로 변경 (데이터 출처와 일치시키기)
|
||||
2. 또는 테넌트 업종에 따라 path 동적 분기 (건설: `/construction/order/order-management`, 기타: `/accounting/purchase`)
|
||||
3. 라벨도 재검토: "발주"가 맞는지, "매입(임시저장)"이 더 정확한지
|
||||
|
||||
#### 이슈 B: 데이터 정합성 의심
|
||||
**현상**: StatusBoard API에서 `purchases` count=**9건** 반환, 하지만 매입관리 페이지(`/accounting/purchase`)에서 전체 조회 시 **1건**만 표시.
|
||||
|
||||
**확인 사항** (DB 직접 확인 필요):
|
||||
```sql
|
||||
-- 현재 테넌트의 purchases 테이블 전체 건수
|
||||
SELECT COUNT(*), status FROM purchases WHERE tenant_id = {현재 테넌트 ID} GROUP BY status;
|
||||
|
||||
-- draft 상태 건수 (StatusBoard가 조회하는 조건)
|
||||
SELECT COUNT(*) FROM purchases WHERE tenant_id = {현재 테넌트 ID} AND status = 'draft';
|
||||
```
|
||||
|
||||
**가능한 원인**:
|
||||
1. StatusBoard와 매입관리 페이지가 다른 tenant_id 스코프로 조회
|
||||
2. DummyDataSeeder가 다른 tenant_id로 데이터 생성
|
||||
3. 매입관리 API에 추가 필터 조건이 있어서 draft 건이 제외됨
|
||||
4. StatusBoard가 실제와 다른 데이터를 집계
|
||||
|
||||
**기대 결과**: StatusBoard 9건 클릭 → 매입관리 페이지에서 9건 확인 가능해야 함
|
||||
|
||||
---
|
||||
|
||||
## 신규 API (10개)
|
||||
|
||||
### 1. 매출 현황 Summary
|
||||
**우선순위**: 중
|
||||
**페이지**: p39
|
||||
|
||||
```
|
||||
GET /api/v1/dashboard/sales/summary
|
||||
```
|
||||
|
||||
**Query Params**:
|
||||
| 파라미터 | 타입 | 필수 | 설명 |
|
||||
|---------|------|------|------|
|
||||
| year | int | N | 조회 연도 (기본: 당해) |
|
||||
| month | int | N | 조회 월 (기본: 당월) |
|
||||
|
||||
**Response** (`SalesStatusApiResponse`):
|
||||
```json
|
||||
{
|
||||
"cumulative_sales": 312300000,
|
||||
"achievement_rate": 94.5,
|
||||
"yoy_change": 12.5,
|
||||
"monthly_sales": 312300000,
|
||||
"monthly_trend": [
|
||||
{ "month": "2026-08", "label": "8월", "amount": 250000000 },
|
||||
{ "month": "2026-09", "label": "9월", "amount": 280000000 }
|
||||
],
|
||||
"client_sales": [
|
||||
{ "name": "대한건설", "amount": 95000000 },
|
||||
{ "name": "삼성테크", "amount": 78000000 }
|
||||
],
|
||||
"daily_items": [
|
||||
{
|
||||
"date": "2026-02-01",
|
||||
"client": "대한건설",
|
||||
"item": "스크린 외",
|
||||
"amount": 25000000,
|
||||
"status": "deposited"
|
||||
}
|
||||
],
|
||||
"daily_total": 312300000
|
||||
}
|
||||
```
|
||||
|
||||
**Laravel 힌트**:
|
||||
- 매출: `sales_orders` 합계 (confirmed 상태)
|
||||
- 달성률: 매출 목표 대비 (`sales_targets` 테이블)
|
||||
- YoY: 전년 동월 대비 변화율
|
||||
- 거래처별: GROUP BY vendor_id → TOP 5
|
||||
- status 코드: `deposited` (입금완료), `unpaid` (미입금), `partial` (부분입금)
|
||||
|
||||
---
|
||||
|
||||
### 2. 매입 현황 Summary
|
||||
**우선순위**: 중
|
||||
**페이지**: p40
|
||||
|
||||
```
|
||||
GET /api/v1/dashboard/purchases/summary
|
||||
```
|
||||
|
||||
**Query Params**:
|
||||
| 파라미터 | 타입 | 필수 | 설명 |
|
||||
|---------|------|------|------|
|
||||
| year | int | N | 조회 연도 (기본: 당해) |
|
||||
| month | int | N | 조회 월 (기본: 당월) |
|
||||
|
||||
**Response** (`PurchaseStatusApiResponse`):
|
||||
```json
|
||||
{
|
||||
"cumulative_purchase": 312300000,
|
||||
"unpaid_amount": 312300000,
|
||||
"yoy_change": -12.5,
|
||||
"monthly_trend": [
|
||||
{ "month": "2026-08", "label": "8월", "amount": 180000000 }
|
||||
],
|
||||
"material_ratio": [
|
||||
{ "name": "원자재", "value": 55, "percentage": 55, "color": "#3b82f6" },
|
||||
{ "name": "부자재", "value": 35, "percentage": 35, "color": "#10b981" },
|
||||
{ "name": "소모품", "value": 10, "percentage": 10, "color": "#f59e0b" }
|
||||
],
|
||||
"daily_items": [
|
||||
{
|
||||
"date": "2026-02-01",
|
||||
"supplier": "한국철강",
|
||||
"item": "철판 외",
|
||||
"amount": 45000000,
|
||||
"status": "paid"
|
||||
}
|
||||
],
|
||||
"daily_total": 312300000
|
||||
}
|
||||
```
|
||||
|
||||
**Laravel 힌트**:
|
||||
- 매입: `purchase_orders` 합계
|
||||
- 미결제: 결제 미완료 건 합계
|
||||
- 원자재/부자재/소모품: `item_categories` 기준 분류
|
||||
- status 코드: `paid` (결제완료), `unpaid` (미결제), `partial` (부분결제)
|
||||
|
||||
---
|
||||
|
||||
### 3. 생산 현황 Summary
|
||||
**우선순위**: 상
|
||||
**페이지**: p41
|
||||
|
||||
```
|
||||
GET /api/v1/dashboard/production/summary
|
||||
```
|
||||
|
||||
**Query Params**:
|
||||
| 파라미터 | 타입 | 필수 | 설명 |
|
||||
|---------|------|------|------|
|
||||
| date | string | N | 조회 일자 (기본: 오늘, YYYY-MM-DD) |
|
||||
|
||||
**Response** (`DailyProductionApiResponse`):
|
||||
```json
|
||||
{
|
||||
"date": "2026-02-23",
|
||||
"day_of_week": "월요일",
|
||||
"processes": [
|
||||
{
|
||||
"process_name": "스크린",
|
||||
"total_work": 10,
|
||||
"todo": 3,
|
||||
"in_progress": 4,
|
||||
"completed": 3,
|
||||
"urgent": 2,
|
||||
"sub_line": 1,
|
||||
"regular": 5,
|
||||
"worker_count": 8,
|
||||
"work_items": [
|
||||
{
|
||||
"id": "wo_1",
|
||||
"order_no": "SO-2026-001",
|
||||
"client": "대한건설",
|
||||
"product": "스크린 A형",
|
||||
"quantity": 50,
|
||||
"status": "in_progress"
|
||||
}
|
||||
],
|
||||
"workers": [
|
||||
{
|
||||
"name": "김철수",
|
||||
"assigned": 5,
|
||||
"completed": 3,
|
||||
"rate": 60
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"shipment": {
|
||||
"expected_amount": 150000000,
|
||||
"expected_count": 12,
|
||||
"actual_amount": 120000000,
|
||||
"actual_count": 9
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Laravel 힌트**:
|
||||
- 공정: `work_processes` 테이블 (스크린, 슬랫, 절곡 등)
|
||||
- 작업: `work_orders` JOIN `work_process_id`
|
||||
- status: `pending` → todo, `in_progress`, `completed`
|
||||
- urgent: 납기 3일 이내
|
||||
- 출고: `shipments` 테이블 (당일 예상 vs 실적)
|
||||
|
||||
---
|
||||
|
||||
### 4. 출고 현황 (생산 현황에 포함)
|
||||
**우선순위**: 하
|
||||
**페이지**: p41
|
||||
|
||||
생산 현황 API의 `shipment` 필드로 포함됨. 별도 API 불필요.
|
||||
|
||||
---
|
||||
|
||||
### 5. 미출고 내역
|
||||
**우선순위**: 하
|
||||
**페이지**: p42
|
||||
|
||||
```
|
||||
GET /api/v1/dashboard/unshipped/summary
|
||||
```
|
||||
|
||||
**Query Params**:
|
||||
| 파라미터 | 타입 | 필수 | 설명 |
|
||||
|---------|------|------|------|
|
||||
| days | int | N | 납기 N일 이내 (기본: 30) |
|
||||
|
||||
**Response** (`UnshippedApiResponse`):
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "us_1",
|
||||
"port_no": "P-2026-001",
|
||||
"site_name": "강남 현장",
|
||||
"order_client": "대한건설",
|
||||
"due_date": "2026-02-25",
|
||||
"days_left": 2
|
||||
}
|
||||
],
|
||||
"total_count": 7
|
||||
}
|
||||
```
|
||||
|
||||
**Laravel 힌트**:
|
||||
- `shipment_items` WHERE shipped_at IS NULL AND due_date >= NOW()
|
||||
- days_left: DATEDIFF(due_date, NOW())
|
||||
- ORDER BY due_date ASC (납기 임박 순)
|
||||
|
||||
---
|
||||
|
||||
### 6. 시공 현황
|
||||
**우선순위**: 중
|
||||
**페이지**: p42
|
||||
|
||||
```
|
||||
GET /api/v1/dashboard/construction/summary
|
||||
```
|
||||
|
||||
**Query Params**:
|
||||
| 파라미터 | 타입 | 필수 | 설명 |
|
||||
|---------|------|------|------|
|
||||
| month | int | N | 조회 월 (기본: 당월) |
|
||||
|
||||
**Response** (`ConstructionApiResponse`):
|
||||
```json
|
||||
{
|
||||
"this_month": 15,
|
||||
"completed": 5,
|
||||
"items": [
|
||||
{
|
||||
"id": "cs_1",
|
||||
"site_name": "강남 현장",
|
||||
"client": "대한건설",
|
||||
"start_date": "2026-02-01",
|
||||
"end_date": "2026-02-28",
|
||||
"progress": 85,
|
||||
"status": "in_progress"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Laravel 힌트**:
|
||||
- `constructions` 테이블
|
||||
- status: `in_progress`, `scheduled`, `completed`
|
||||
- completed: 최근 7일 이내 완료 건
|
||||
|
||||
---
|
||||
|
||||
### 7. 근태 현황
|
||||
**우선순위**: 중
|
||||
**페이지**: p43
|
||||
|
||||
```
|
||||
GET /api/v1/dashboard/attendance/summary
|
||||
```
|
||||
|
||||
**Query Params**:
|
||||
| 파라미터 | 타입 | 필수 | 설명 |
|
||||
|---------|------|------|------|
|
||||
| date | string | N | 조회 일자 (기본: 오늘, YYYY-MM-DD) |
|
||||
|
||||
**Response** (`DailyAttendanceApiResponse`):
|
||||
```json
|
||||
{
|
||||
"present": 42,
|
||||
"on_leave": 3,
|
||||
"late": 1,
|
||||
"absent": 0,
|
||||
"employees": [
|
||||
{
|
||||
"id": "emp_1",
|
||||
"department": "생산부",
|
||||
"position": "과장",
|
||||
"name": "김철수",
|
||||
"status": "present"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Laravel 힌트**:
|
||||
- `attendances` WHERE date = :date
|
||||
- status: `present`, `on_leave`, `late`, `absent`
|
||||
- employees: 이상 상태(late, absent, on_leave) 위주 표시
|
||||
|
||||
---
|
||||
|
||||
### 8. 일별 매출 내역
|
||||
**우선순위**: 하
|
||||
**페이지**: p47 (설정 팝업에서 별도 ON/OFF)
|
||||
|
||||
매출 현황 API의 `daily_items`로 이미 포함. 별도 API 필요 시:
|
||||
|
||||
```
|
||||
GET /api/v1/dashboard/sales/daily
|
||||
```
|
||||
|
||||
**Query Params**:
|
||||
| 파라미터 | 타입 | 필수 | 설명 |
|
||||
|---------|------|------|------|
|
||||
| start_date | string | N | 시작일 (기본: 당월 1일) |
|
||||
| end_date | string | N | 종료일 (기본: 오늘) |
|
||||
| page | int | N | 페이지 (기본: 1) |
|
||||
| per_page | int | N | 건수 (기본: 20) |
|
||||
|
||||
---
|
||||
|
||||
### 9. 일별 매입 내역
|
||||
**우선순위**: 하
|
||||
|
||||
매입 현황 API의 `daily_items`로 이미 포함. 별도 API 필요 시:
|
||||
|
||||
```
|
||||
GET /api/v1/dashboard/purchases/daily
|
||||
```
|
||||
|
||||
(매출 일별과 동일 구조)
|
||||
|
||||
---
|
||||
|
||||
### 10. 접대비 상세
|
||||
**우선순위**: 상
|
||||
**페이지**: p53-54
|
||||
|
||||
```
|
||||
GET /api/v1/dashboard/entertainment/detail
|
||||
```
|
||||
|
||||
**Query Params**:
|
||||
| 파라미터 | 타입 | 필수 | 설명 |
|
||||
|---------|------|------|------|
|
||||
| year | int | N | 연도 |
|
||||
| quarter | int | N | 분기 (1-4) |
|
||||
| limit_type | string | N | annual/quarterly |
|
||||
| company_type | string | N | large/medium/small |
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"summary": {
|
||||
"total_used": 10000000,
|
||||
"annual_limit": 40120000,
|
||||
"remaining": 30120000,
|
||||
"usage_rate": 24.9
|
||||
},
|
||||
"limit_calculation": {
|
||||
"base_limit": 36000000,
|
||||
"revenue_additional": 4120000,
|
||||
"total_limit": 40120000,
|
||||
"revenue": 2060000000,
|
||||
"company_type": "medium"
|
||||
},
|
||||
"quarterly_status": [
|
||||
{
|
||||
"quarter": 1,
|
||||
"label": "1분기",
|
||||
"limit": 10030000,
|
||||
"used": 3500000,
|
||||
"remaining": 6530000,
|
||||
"exceeded": 0
|
||||
}
|
||||
],
|
||||
"transactions": [
|
||||
{
|
||||
"id": 1,
|
||||
"date": "2026-01-15",
|
||||
"user_name": "홍길동",
|
||||
"merchant_name": "강남식당",
|
||||
"amount": 350000,
|
||||
"counterpart": "대한건설",
|
||||
"receipt_type": "법인카드",
|
||||
"risk_flags": ["high_amount"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 수정 API (6개)
|
||||
|
||||
### 1. 가지급금 Summary (수정)
|
||||
**현재**: 카드/가지급금/법인세/종합세
|
||||
**변경**: 카드/경조사/상품권/접대비/총합계 (5카드)
|
||||
|
||||
```
|
||||
GET /api/proxy/card-transactions/summary
|
||||
```
|
||||
|
||||
**Response 변경**:
|
||||
```json
|
||||
{
|
||||
"cards": [
|
||||
{ "id": "cm1", "label": "카드", "amount": 3123000, "sub_label": "미정리 5건", "count": 5 },
|
||||
{ "id": "cm2", "label": "경조사", "amount": 3123000, "sub_label": "미증빙 5건", "count": 5 },
|
||||
{ "id": "cm3", "label": "상품권", "amount": 3123000, "sub_label": "미증빙 5건", "count": 5 },
|
||||
{ "id": "cm4", "label": "접대비", "amount": 3123000, "sub_label": "미증빙 5건", "count": 5 },
|
||||
{ "id": "cm_total", "label": "총 가지급금 합계", "amount": 350000000 }
|
||||
],
|
||||
"check_points": [
|
||||
{
|
||||
"id": "cm-cp1",
|
||||
"type": "warning",
|
||||
"message": "법인카드 사용 총 850만원이 가지급금으로 전환되었습니다.",
|
||||
"highlights": [{ "text": "850만원", "color": "red" }]
|
||||
}
|
||||
],
|
||||
"warning_banner": "가지급금 인정이자 4.6%, 법인세 및 연말정산 시 대표자 종합세 가중 주의"
|
||||
}
|
||||
```
|
||||
|
||||
**Laravel 힌트**:
|
||||
- 분류: `card_transactions.category` 기준 (card/congratulation/gift_card/entertainment)
|
||||
- 미정리/미증빙: `evidence_status = 'pending'` COUNT
|
||||
|
||||
---
|
||||
|
||||
### 2. 접대비 Summary (수정)
|
||||
**현재**: 매출/한도/잔여한도/사용금액
|
||||
**변경**: 주말심야/기피업종/고액결제/증빙미비 (리스크 4종)
|
||||
|
||||
```
|
||||
GET /api/proxy/entertainment/summary
|
||||
```
|
||||
|
||||
**Response 변경**:
|
||||
```json
|
||||
{
|
||||
"cards": [
|
||||
{ "id": "et1", "label": "주말/심야", "amount": 3123000, "sub_label": "5건", "count": 5 },
|
||||
{ "id": "et2", "label": "기피업종 (유흥, 귀금속 등)", "amount": 3123000, "sub_label": "불인정 5건", "count": 5 },
|
||||
{ "id": "et3", "label": "고액 결제", "amount": 3123000, "sub_label": "5건", "count": 5 },
|
||||
{ "id": "et4", "label": "증빙 미비", "amount": 3123000, "sub_label": "5건", "count": 5 }
|
||||
],
|
||||
"check_points": [...]
|
||||
}
|
||||
```
|
||||
|
||||
**리스크 감지 로직** (p60 참조):
|
||||
- 주말/심야: 토~일, 22:00~06:00 거래
|
||||
- 기피업종: MCC 코드 기반 (유흥업소 7273, 귀금속 5944, 골프장 7941 등)
|
||||
- 고액 결제: 설정 금액(기본 50만원) 초과
|
||||
- 증빙 미비: 적격증빙(세금계산서/카드매출전표) 없는 건
|
||||
|
||||
---
|
||||
|
||||
### 3. 복리후생비 Summary (수정)
|
||||
**현재**: 한도/잔여한도/사용금액
|
||||
**변경**: 비과세한도초과/사적사용의심/특정인편중/항목별한도초과 (리스크 4종)
|
||||
|
||||
```
|
||||
GET /api/proxy/welfare/summary
|
||||
```
|
||||
|
||||
**Response 변경**:
|
||||
```json
|
||||
{
|
||||
"cards": [
|
||||
{ "id": "wf1", "label": "비과세 한도 초과", "amount": 3123000, "sub_label": "5건", "count": 5 },
|
||||
{ "id": "wf2", "label": "사적 사용 의심", "amount": 3123000, "sub_label": "5건", "count": 5 },
|
||||
{ "id": "wf3", "label": "특정인 편중", "amount": 3123000, "sub_label": "5건", "count": 5 },
|
||||
{ "id": "wf4", "label": "항목별 한도 초과", "amount": 3123000, "sub_label": "5건", "count": 5 }
|
||||
],
|
||||
"check_points": [...]
|
||||
}
|
||||
```
|
||||
|
||||
**리스크 감지 로직**:
|
||||
- 비과세 한도 초과: 항목별 비과세 기준 초과 (식대 20만원, 교통비 10만원 등)
|
||||
- 사적 사용 의심: 주말/야간 + 비업무 업종 조합
|
||||
- 특정인 편중: 직원별 사용액 편차 > 평균의 200%
|
||||
- 항목별 한도 초과: 설정 금액 초과
|
||||
|
||||
---
|
||||
|
||||
### 4. 가지급금 Detail (수정)
|
||||
|
||||
기존 `LoanDashboardApiResponse`에 AI분류 컬럼 추가.
|
||||
|
||||
```
|
||||
GET /api/v1/loans/dashboard
|
||||
```
|
||||
|
||||
**Response 추가 필드**:
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"...기존 필드...",
|
||||
"ai_category": "카드",
|
||||
"evidence_status": "미증빙"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 복리후생비 Detail (수정)
|
||||
|
||||
기존 `WelfareDetailApiResponse`에 계산방식 파라미터 추가.
|
||||
|
||||
```
|
||||
GET /api/proxy/welfare/detail?calculation_type=fixed&fixed_amount_per_month=200000
|
||||
```
|
||||
|
||||
(기존 구현 유지, 계산 파라미터만 반영 확인)
|
||||
|
||||
---
|
||||
|
||||
### 6. 부가세 Detail (수정)
|
||||
|
||||
기존 `VatApiResponse`에 신고기간 파라미터 반영.
|
||||
|
||||
```
|
||||
GET /api/proxy/vat/summary?period_type=quarter&year=2026&period=1
|
||||
```
|
||||
|
||||
(기존 구현 유지, 기간별 필터링 확인)
|
||||
|
||||
---
|
||||
|
||||
## 리스크 감지 로직 참고 (p58-60)
|
||||
|
||||
### MCC 코드 기피업종
|
||||
| MCC | 업종 | 분류 |
|
||||
|-----|------|------|
|
||||
| 7273 | 유흥업소 | 기피업종 |
|
||||
| 5944 | 귀금속 | 기피업종 |
|
||||
| 7941 | 골프장 | 기피업종 |
|
||||
| 5813 | 주점 | 기피업종 |
|
||||
| 7011 | 호텔/리조트 | 주의업종 |
|
||||
|
||||
### 리스크 판별 규칙
|
||||
```
|
||||
규칙1: 시간대 이상 → 22:00~06:00 또는 토~일
|
||||
규칙2: 업종 이상 → MCC 기피업종 해당
|
||||
규칙3: 금액 이상 → 설정 금액 초과 (기본 50만원)
|
||||
규칙4: 빈도 이상 → 월 10회 이상 동일 업종
|
||||
규칙5: 증빙 미비 → 적격증빙 없음
|
||||
|
||||
리스크 등급:
|
||||
- 2개 이상 해당 → 🔴 고위험
|
||||
- 1개 해당 → 🟡 주의
|
||||
- 0개 → 🟢 정상
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 계산 공식 참고
|
||||
|
||||
### 가지급금 인정이자 (p58)
|
||||
```
|
||||
인정이자 = 가지급금잔액 × (4.6% / 365) × 경과일수
|
||||
법인세 추가 = 인정이자 × 19%
|
||||
대표자 소득세 = 인정이자 × 35%
|
||||
```
|
||||
|
||||
### 접대비 손금한도 (p59)
|
||||
```
|
||||
기본한도:
|
||||
일반법인: 1,200만원/년
|
||||
중소기업: 3,600만원/년
|
||||
|
||||
수입금액별 추가:
|
||||
100억 이하: 수입금액 × 0.2%
|
||||
100~500억: 2,000만원 + (수입금액-100억) × 0.1%
|
||||
500억 초과: 6,000만원 + (수입금액-500억) × 0.03%
|
||||
```
|
||||
|
||||
### 복리후생비 (p60)
|
||||
```
|
||||
방식1 (정액): 직원수 × 월정액 × 12
|
||||
방식2 (비율): 연봉총액 × 비율%
|
||||
|
||||
비과세 한도:
|
||||
식대: 20만원/월
|
||||
교통비: 10만원/월
|
||||
경조사: 5만원/건
|
||||
건강검진: 연간 총액/12 환산
|
||||
교육훈련: 8만원/월
|
||||
복지포인트: 10만원/월
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 우선순위 정리
|
||||
|
||||
| 우선순위 | API | 이유 |
|
||||
|---------|-----|------|
|
||||
| 🔴 상 | 접대비 summary 수정, 복리후생비 summary 수정 | D1.7 카드 구조 변경 |
|
||||
| 🔴 상 | 가지급금 summary 수정 | D1.7 카드 구조 변경 |
|
||||
| 🔴 상 | 접대비 detail 신규 | 모달 확장 |
|
||||
| 🟡 중 | 매출 현황, 매입 현황, 시공 현황, 근태 현황 | 신규 섹션 |
|
||||
| 🟡 중 | 생산 현황 | 복잡한 공정 집계 |
|
||||
| 🟢 하 | 미출고 내역, 일별 매출/매입 | 단순 조회 |
|
||||
262
claudedocs/api/[IMPL-2025-12-30] fetch-wrapper-migration.md
Normal file
262
claudedocs/api/[IMPL-2025-12-30] fetch-wrapper-migration.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# Fetch Wrapper Migration Checklist
|
||||
|
||||
**생성일**: 2025-12-30
|
||||
**목적**: 모든 Server Actions의 API 통신을 `serverFetch`로 중앙화
|
||||
|
||||
## 목적 및 배경
|
||||
|
||||
### 왜 fetch-wrapper를 도입했는가?
|
||||
|
||||
1. **중앙화된 인증 처리**
|
||||
- 401 에러(세션 만료) 발생 시 → 로그인 페이지 리다이렉트
|
||||
- 모든 API 호출에서 **일관된 인증 검증**
|
||||
|
||||
2. **개발 규칙 표준화**
|
||||
- 새 작업자도 `serverFetch` 사용하면 자동으로 인증 검증 적용
|
||||
- 개별 파일마다 인증 로직 구현 불필요
|
||||
|
||||
3. **유지보수성 향상**
|
||||
- 인증 로직 변경 시 **`fetch-wrapper.ts` 한 파일만** 수정
|
||||
- 403, 네트워크 에러 등 공통 에러 처리도 중앙화
|
||||
|
||||
---
|
||||
|
||||
## 마이그레이션 패턴
|
||||
|
||||
### Before (기존 패턴)
|
||||
```typescript
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
return {
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
// ...
|
||||
};
|
||||
}
|
||||
|
||||
export async function getSomething() {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(url, { headers });
|
||||
// 401 처리 없음!
|
||||
}
|
||||
```
|
||||
|
||||
### After (새 패턴)
|
||||
```typescript
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
|
||||
export async function getSomething() {
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error) {
|
||||
// 401/403/네트워크 에러 자동 처리됨
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 마이그레이션 체크리스트
|
||||
|
||||
### Accounting 도메인 (12 files) ✅ 완료
|
||||
- [x] `SalesManagement/actions.ts`
|
||||
- [x] `VendorManagement/actions.ts`
|
||||
- [x] `PurchaseManagement/actions.ts`
|
||||
- [x] `DepositManagement/actions.ts`
|
||||
- [x] `WithdrawalManagement/actions.ts`
|
||||
- [x] `VendorLedger/actions.ts`
|
||||
- [x] `ReceivablesStatus/actions.ts`
|
||||
- [x] `ExpectedExpenseManagement/actions.ts`
|
||||
- [x] `CardTransactionInquiry/actions.ts`
|
||||
- [x] `DailyReport/actions.ts`
|
||||
- [x] `BadDebtCollection/actions.ts`
|
||||
- [x] `BankTransactionInquiry/actions.ts`
|
||||
|
||||
### HR 도메인 (6 files) ✅ 완료
|
||||
- [x] `EmployeeManagement/actions.ts` ✅ (이미 마이그레이션됨)
|
||||
- [x] `VacationManagement/actions.ts` ✅
|
||||
- [x] `SalaryManagement/actions.ts` ✅
|
||||
- [x] `CardManagement/actions.ts` ✅
|
||||
- [x] `DepartmentManagement/actions.ts` ✅
|
||||
- [x] `AttendanceManagement/actions.ts` ✅
|
||||
|
||||
### Approval 도메인 (4 files) ✅ 완료
|
||||
- [x] `ApprovalBox/actions.ts`
|
||||
- [x] `DraftBox/actions.ts`
|
||||
- [x] `ReferenceBox/actions.ts`
|
||||
- [x] `DocumentCreate/actions.ts` (파일 업로드는 직접 fetch 유지)
|
||||
|
||||
### Production 도메인 (4 files) ✅ 완료
|
||||
- [x] `WorkerScreen/actions.ts`
|
||||
- [x] `WorkOrders/actions.ts`
|
||||
- [x] `WorkResults/actions.ts`
|
||||
- [x] `ProductionDashboard/actions.ts`
|
||||
|
||||
### Settings 도메인 (10 files) ✅ 완료
|
||||
- [x] `WorkScheduleManagement/actions.ts`
|
||||
- [x] `SubscriptionManagement/actions.ts`
|
||||
- [x] `PopupManagement/actions.ts`
|
||||
- [x] `PaymentHistoryManagement/actions.ts`
|
||||
- [x] `LeavePolicyManagement/actions.ts`
|
||||
- [x] `NotificationSettings/actions.ts`
|
||||
- [x] `AttendanceSettingsManagement/actions.ts`
|
||||
- [x] `CompanyInfoManagement/actions.ts`
|
||||
- [x] `AccountInfoManagement/actions.ts`
|
||||
- [x] `AccountManagement/actions.ts`
|
||||
|
||||
### 기타 도메인 (12 files) ✅ 완료
|
||||
- [x] `process-management/actions.ts`
|
||||
- [x] `outbound/ShipmentManagement/actions.ts`
|
||||
- [x] `material/StockStatus/actions.ts`
|
||||
- [x] `material/ReceivingManagement/actions.ts`
|
||||
- [x] `customer-center/shared/actions.ts`
|
||||
- [x] `board/actions.ts`
|
||||
- [x] `reports/actions.ts`
|
||||
- [x] `quotes/actions.ts`
|
||||
- [x] `board/BoardManagement/actions.ts`
|
||||
- [x] `attendance/actions.ts`
|
||||
- [x] `pricing/actions.ts`
|
||||
- [x] `quality/InspectionManagement/actions.ts`
|
||||
|
||||
---
|
||||
|
||||
## 진행 상황
|
||||
|
||||
| 도메인 | 파일 수 | 완료 | 상태 |
|
||||
|--------|---------|------|------|
|
||||
| Accounting | 12 | 12 | ✅ 완료 |
|
||||
| HR | 6 | 6 | ✅ 완료 |
|
||||
| Approval | 4 | 4 | ✅ 완료 |
|
||||
| Production | 4 | 4 | ✅ 완료 |
|
||||
| Settings | 10 | 10 | ✅ 완료 |
|
||||
| 기타 | 12 | 12 | ✅ 완료 |
|
||||
| **총계** | **48** | **48** | **100%** ✅ |
|
||||
|
||||
### 완료된 파일 (완전 마이그레이션)
|
||||
|
||||
**Accounting 도메인 (12/12)**
|
||||
- [x] `SalesManagement/actions.ts`
|
||||
- [x] `VendorManagement/actions.ts`
|
||||
- [x] `PurchaseManagement/actions.ts`
|
||||
- [x] `DepositManagement/actions.ts`
|
||||
- [x] `WithdrawalManagement/actions.ts`
|
||||
- [x] `VendorLedger/actions.ts`
|
||||
- [x] `ReceivablesStatus/actions.ts`
|
||||
- [x] `ExpectedExpenseManagement/actions.ts`
|
||||
- [x] `CardTransactionInquiry/actions.ts`
|
||||
- [x] `DailyReport/actions.ts`
|
||||
- [x] `BadDebtCollection/actions.ts`
|
||||
- [x] `BankTransactionInquiry/actions.ts`
|
||||
|
||||
**HR 도메인 (6/6)**
|
||||
- [x] `EmployeeManagement/actions.ts` (이미 마이그레이션됨)
|
||||
- [x] `VacationManagement/actions.ts`
|
||||
- [x] `SalaryManagement/actions.ts`
|
||||
- [x] `CardManagement/actions.ts`
|
||||
- [x] `DepartmentManagement/actions.ts`
|
||||
- [x] `AttendanceManagement/actions.ts`
|
||||
|
||||
**Approval 도메인 (4/4)**
|
||||
- [x] `ApprovalBox/actions.ts`
|
||||
- [x] `DraftBox/actions.ts`
|
||||
- [x] `ReferenceBox/actions.ts`
|
||||
- [x] `DocumentCreate/actions.ts` (파일 업로드는 직접 fetch 유지)
|
||||
|
||||
**Production 도메인 (4/4)**
|
||||
- [x] `WorkerScreen/actions.ts`
|
||||
- [x] `WorkOrders/actions.ts`
|
||||
- [x] `WorkResults/actions.ts`
|
||||
- [x] `ProductionDashboard/actions.ts`
|
||||
|
||||
**Settings 도메인 (10/10)**
|
||||
- [x] `WorkScheduleManagement/actions.ts`
|
||||
- [x] `SubscriptionManagement/actions.ts`
|
||||
- [x] `PopupManagement/actions.ts`
|
||||
- [x] `PaymentHistoryManagement/actions.ts`
|
||||
- [x] `LeavePolicyManagement/actions.ts`
|
||||
- [x] `NotificationSettings/actions.ts`
|
||||
- [x] `AttendanceSettingsManagement/actions.ts`
|
||||
- [x] `CompanyInfoManagement/actions.ts`
|
||||
- [x] `AccountInfoManagement/actions.ts`
|
||||
- [x] `AccountManagement/actions.ts`
|
||||
|
||||
**기타 도메인 (12/12)** ✅ 완료
|
||||
- [x] `process-management/actions.ts`
|
||||
- [x] `outbound/ShipmentManagement/actions.ts`
|
||||
- [x] `material/StockStatus/actions.ts`
|
||||
- [x] `material/ReceivingManagement/actions.ts`
|
||||
- [x] `customer-center/shared/actions.ts`
|
||||
- [x] `board/actions.ts`
|
||||
- [x] `reports/actions.ts`
|
||||
- [x] `quotes/actions.ts`
|
||||
- [x] `board/BoardManagement/actions.ts`
|
||||
- [x] `attendance/actions.ts`
|
||||
- [x] `pricing/actions.ts`
|
||||
- [x] `quality/InspectionManagement/actions.ts`
|
||||
|
||||
---
|
||||
|
||||
## 참조 파일
|
||||
|
||||
- **fetch-wrapper**: `src/lib/api/fetch-wrapper.ts`
|
||||
- **errors**: `src/lib/api/errors.ts`
|
||||
- **완료된 예시**: `src/components/accounting/BillManagement/actions.ts` (참고용)
|
||||
|
||||
---
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **기존 `getApiHeaders()` 함수 제거** - `serverFetch`가 헤더 자동 생성
|
||||
2. **`import { cookies } from 'next/headers'` 제거** - wrapper에서 처리
|
||||
3. **에러 응답 구조 맞추기** - `{ success: false, error: string }` 형태 유지
|
||||
4. **빌드 테스트 필수** - 마이그레이션 후 `npm run build` 확인
|
||||
|
||||
---
|
||||
|
||||
## 🔜 추가 작업 (마이그레이션 완료 후)
|
||||
|
||||
### Phase 2: 리프레시 토큰 자동 갱신 적용
|
||||
|
||||
**현재 문제:**
|
||||
- access_token 만료 시 (약 2시간) 바로 로그인 리다이렉트됨
|
||||
- refresh_token (7일)을 사용한 자동 갱신 로직이 호출되지 않음
|
||||
- 결과: 40분~2시간 후 세션 만료 → 재로그인 필요
|
||||
|
||||
**목표:**
|
||||
- 401 발생 시 → 리프레시 토큰으로 갱신 시도 → 성공 시 재시도
|
||||
- 7일간 세션 유지 (refresh_token 만료 시에만 재로그인)
|
||||
|
||||
**적용 범위:**
|
||||
|
||||
| 영역 | 적용 위치 | 작업 |
|
||||
|------|----------|------|
|
||||
| Server Actions | `fetch-wrapper.ts` | 401 시 리프레시 후 재시도 로직 추가 |
|
||||
| 품목관리 | `ItemListClient.tsx` 등 | 클라이언트 fetch에 리프레시 로직 추가 |
|
||||
| 품목기준관리 | 관련 컴포넌트들 | 클라이언트 fetch에 리프레시 로직 추가 |
|
||||
|
||||
**관련 파일:**
|
||||
- `src/lib/auth/token-refresh.ts` - 리프레시 함수 (이미 존재)
|
||||
- `src/app/api/auth/refresh/route.ts` - 리프레시 API (이미 존재)
|
||||
|
||||
**예상 구현:**
|
||||
```typescript
|
||||
// fetch-wrapper.ts 401 처리 부분
|
||||
if (response.status === 401 && !options?.skipAuthCheck) {
|
||||
// 1. 리프레시 토큰으로 갱신 시도
|
||||
const refreshResult = await refreshTokenServer(refreshToken);
|
||||
|
||||
if (refreshResult.success) {
|
||||
// 2. 새 토큰으로 원래 요청 재시도
|
||||
return serverFetch(url, { ...options, skipAuthCheck: true });
|
||||
}
|
||||
|
||||
// 3. 리프레시도 실패하면 로그인 리다이렉트
|
||||
redirect('/login');
|
||||
}
|
||||
```
|
||||
BIN
claudedocs/architecture/.DS_Store
vendored
Normal file
BIN
claudedocs/architecture/.DS_Store
vendored
Normal file
Binary file not shown.
213
claudedocs/architecture/[ANALYSIS-2026-01-20] 공통화-현황-분석.md
Normal file
213
claudedocs/architecture/[ANALYSIS-2026-01-20] 공통화-현황-분석.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# 프로젝트 공통화 현황 분석
|
||||
|
||||
## 1. 핵심 지표 요약
|
||||
|
||||
| 구분 | 적용 현황 | 비고 |
|
||||
|------|----------|------|
|
||||
| **IntegratedDetailTemplate** | 96개 파일 (228회 사용) | 상세/수정/등록 페이지 통합 |
|
||||
| **IntegratedListTemplateV2** | 50개 파일 (60회 사용) | 목록 페이지 통합 |
|
||||
| **DetailConfig 파일** | 39개 생성 | 설정 기반 페이지 구성 |
|
||||
| **레거시 패턴 (PageLayout 직접 사용)** | ~40-50개 파일 | 마이그레이션 대상 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 공통화 달성률
|
||||
|
||||
### 2.1 상세 페이지 (Detail)
|
||||
```
|
||||
총 Detail 컴포넌트: ~105개
|
||||
IntegratedDetailTemplate 적용: ~65개
|
||||
적용률: 약 62%
|
||||
```
|
||||
|
||||
### 2.2 목록 페이지 (List)
|
||||
```
|
||||
총 List 컴포넌트: ~61개
|
||||
IntegratedListTemplateV2 적용: ~50개
|
||||
적용률: 약 82%
|
||||
```
|
||||
|
||||
### 2.3 폼 컴포넌트 (Form)
|
||||
```
|
||||
총 Form 컴포넌트: ~72개
|
||||
공통 템플릿 미적용 (개별 구현)
|
||||
적용률: 0%
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 잘 공통화된 영역 ✅
|
||||
|
||||
### 3.1 템플릿 시스템
|
||||
| 템플릿 | 용도 | 적용 현황 |
|
||||
|--------|------|----------|
|
||||
| IntegratedDetailTemplate | 상세/수정/등록 | 96개 파일 |
|
||||
| IntegratedListTemplateV2 | 목록 페이지 | 50개 파일 |
|
||||
| UniversalListPage | 범용 목록 | 7개 파일 |
|
||||
|
||||
### 3.2 UI 컴포넌트 (Radix UI 기반)
|
||||
- **AlertDialog**: 65개 파일에서 일관되게 사용
|
||||
- **Dialog**: 142개 파일에서 사용
|
||||
- **Toast (Sonner)**: 133개 파일에서 일관되게 사용
|
||||
- **Pagination**: 54개 파일에서 통합 사용
|
||||
|
||||
### 3.3 데이터 테이블
|
||||
- **DataTable**: 공통 컴포넌트로 추상화됨
|
||||
- **IntegratedListTemplateV2에 통합**: 자동 페이지네이션, 필터링
|
||||
|
||||
---
|
||||
|
||||
## 4. 추가 공통화 기회 🔧
|
||||
|
||||
### 4.1 우선순위 높음 (High Priority)
|
||||
|
||||
#### 📋 Form 템플릿 (IntegratedFormTemplate)
|
||||
**현황**: 72개 Form 컴포넌트가 개별적으로 구현됨
|
||||
**제안**:
|
||||
```typescript
|
||||
// 제안: IntegratedFormTemplate
|
||||
<IntegratedFormTemplate
|
||||
config={formConfig}
|
||||
mode="create" | "edit"
|
||||
initialData={data}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={handleCancel}
|
||||
renderFields={() => <CustomFields />}
|
||||
/>
|
||||
```
|
||||
**효과**:
|
||||
- 폼 레이아웃 일관성
|
||||
- 버튼 영역 통합 (저장/취소/삭제)
|
||||
- 유효성 검사 패턴 통합
|
||||
|
||||
#### 📝 레거시 페이지 마이그레이션
|
||||
**현황**: ~40-50개 파일이 PageLayout/PageHeader 직접 사용
|
||||
**대상 파일** (샘플):
|
||||
- `SubscriptionClient.tsx`
|
||||
- `SubscriptionManagement.tsx`
|
||||
- `ComprehensiveAnalysis/index.tsx`
|
||||
- `DailyReport/index.tsx`
|
||||
- `ReceivablesStatus/index.tsx`
|
||||
- `FAQManagement/FAQList.tsx`
|
||||
- `DepartmentManagement/index.tsx`
|
||||
- 등등
|
||||
|
||||
---
|
||||
|
||||
### 4.2 우선순위 중간 (Medium Priority)
|
||||
|
||||
#### 🗑️ 삭제 확인 다이얼로그 통합
|
||||
**현황**: 각 컴포넌트에서 AlertDialog 반복 구현
|
||||
**제안**:
|
||||
```typescript
|
||||
// 제안: useDeleteConfirm hook
|
||||
const { openDeleteConfirm, DeleteConfirmDialog } = useDeleteConfirm({
|
||||
title: '삭제 확인',
|
||||
description: '정말 삭제하시겠습니까?',
|
||||
onConfirm: handleDelete,
|
||||
});
|
||||
|
||||
// 또는 공통 컴포넌트
|
||||
<DeleteConfirmDialog
|
||||
isOpen={isOpen}
|
||||
itemName={itemName}
|
||||
onConfirm={handleDelete}
|
||||
onCancel={() => setIsOpen(false)}
|
||||
/>
|
||||
```
|
||||
|
||||
#### 📁 파일 업로드/다운로드 패턴 통합
|
||||
**현황**: 여러 컴포넌트에서 파일 처리 로직 중복
|
||||
**제안**:
|
||||
```typescript
|
||||
// 제안: useFileUpload hook
|
||||
const { uploadFile, downloadFile, FileDropzone } = useFileUpload({
|
||||
accept: ['image/*', '.pdf'],
|
||||
maxSize: 10 * 1024 * 1024,
|
||||
});
|
||||
```
|
||||
|
||||
#### 🔄 로딩 상태 표시 통합
|
||||
**현황**: 43개 파일에서 다양한 로딩 패턴 사용
|
||||
**제안**:
|
||||
- `LoadingOverlay` 컴포넌트 확대 적용
|
||||
- `Skeleton` 패턴 표준화
|
||||
|
||||
---
|
||||
|
||||
### 4.3 우선순위 낮음 (Low Priority)
|
||||
|
||||
#### 📊 대시보드 카드 컴포넌트
|
||||
**현황**: CEO 대시보드, 생산 대시보드 등에서 유사 패턴
|
||||
**제안**: `DashboardCard`, `StatCard` 공통 컴포넌트
|
||||
|
||||
#### 🔍 검색/필터 패턴
|
||||
**현황**: IntegratedListTemplateV2에 이미 통합됨
|
||||
**추가**: 독립 검색 컴포넌트 표준화
|
||||
|
||||
---
|
||||
|
||||
## 5. 레거시 파일 정리 대상
|
||||
|
||||
### 5.1 _legacy 폴더 (삭제 검토)
|
||||
```
|
||||
src/components/hr/CardManagement/_legacy/
|
||||
- CardDetail.tsx
|
||||
- CardForm.tsx
|
||||
|
||||
src/components/settings/AccountManagement/_legacy/
|
||||
- AccountDetail.tsx
|
||||
```
|
||||
|
||||
### 5.2 V1/V2 중복 파일 (통합 검토)
|
||||
- `LaborDetailClient.tsx` vs `LaborDetailClientV2.tsx`
|
||||
- `PricingDetailClient.tsx` vs `PricingDetailClientV2.tsx`
|
||||
- `DepositDetail.tsx` vs `DepositDetailClientV2.tsx`
|
||||
- `WithdrawalDetail.tsx` vs `WithdrawalDetailClientV2.tsx`
|
||||
|
||||
---
|
||||
|
||||
## 6. 권장 액션 플랜
|
||||
|
||||
### Phase 7: 레거시 페이지 마이그레이션
|
||||
| 순서 | 대상 | 예상 작업량 |
|
||||
|------|------|------------|
|
||||
| 1 | 설정 관리 페이지 (8개) | 중간 |
|
||||
| 2 | 회계 관리 페이지 (5개) | 중간 |
|
||||
| 3 | 인사 관리 페이지 (5개) | 중간 |
|
||||
| 4 | 보고서/분석 페이지 (3개) | 낮음 |
|
||||
|
||||
### Phase 8: Form 템플릿 개발
|
||||
1. IntegratedFormTemplate 설계
|
||||
2. 파일럿 적용 (2-3개 Form)
|
||||
3. 점진적 마이그레이션
|
||||
|
||||
### Phase 9: 유틸리티 Hook 개발
|
||||
1. useDeleteConfirm
|
||||
2. useFileUpload
|
||||
3. useFormState (공통 폼 상태 관리)
|
||||
|
||||
### Phase 10: 레거시 정리
|
||||
1. _legacy 폴더 삭제
|
||||
2. V1/V2 중복 파일 통합
|
||||
3. 미사용 컴포넌트 정리
|
||||
|
||||
---
|
||||
|
||||
## 7. 결론
|
||||
|
||||
### 공통화 성과
|
||||
- **상세 페이지**: 62% 공통화 달성 (Phase 6 완료)
|
||||
- **목록 페이지**: 82% 공통화 달성
|
||||
- **UI 컴포넌트**: Radix UI 기반 일관성 확보
|
||||
- **토스트/알림**: Sonner로 완전 통합
|
||||
|
||||
### 남은 과제
|
||||
- **Form 템플릿**: 72개 폼 컴포넌트 공통화 필요
|
||||
- **레거시 페이지**: ~40-50개 마이그레이션 필요
|
||||
- **코드 정리**: _legacy, V1/V2 중복 파일 정리
|
||||
|
||||
### 예상 효과 (추가 공통화 시)
|
||||
- 코드 중복 30% 추가 감소
|
||||
- 신규 페이지 개발 시간 50% 단축
|
||||
- 유지보수성 대폭 향상
|
||||
@@ -0,0 +1,256 @@
|
||||
# SAM 프로젝트 정체성 및 현장 효용성 분석
|
||||
|
||||
> 작성일: 2026-02-05
|
||||
> 목적: ERP/MES 관점에서 SAM 시스템의 포지션, 강점/약점, 현장 효용성 분석
|
||||
|
||||
---
|
||||
|
||||
## 1. SAM의 정체성: "제조+설치 통합형 ERP/MES"
|
||||
|
||||
### 포지셔닝
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SAM 시스템 포지션 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Pure MES ◄────────── SAM ──────────► Pure ERP │
|
||||
│ (공장 실행) │ (경영 관리) │
|
||||
│ │ │
|
||||
│ ┌────────┴────────┐ │
|
||||
│ │ 70% ERP │ │
|
||||
│ │ 30% MES │ │
|
||||
│ │ + 건설 프로젝트 │ │
|
||||
│ └─────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
SAM은 **순수 MES도 아니고 순수 ERP도 아닌**, 제조업체가 실제로 필요로 하는 기능들을 통합한 시스템이다.
|
||||
|
||||
### 타겟 산업: 블라인드/셔터 제조 + 설치
|
||||
|
||||
| 특징 | SAM의 대응 |
|
||||
|------|-----------|
|
||||
| 주문생산(Make-to-Order) | 수주 → 생산지시 → 작업실적 흐름 |
|
||||
| 다품종 소량생산 | 동적 품목 마스터 (빌더 시스템) |
|
||||
| 설치 서비스 병행 | 건설/시공 프로젝트 모듈 |
|
||||
| 품질 인증 필요 | QMS 검사성적서 시스템 |
|
||||
| 중소기업 규모 | SaaS 멀티테넌트 구조 |
|
||||
|
||||
---
|
||||
|
||||
## 2. ERP 관점 분석
|
||||
|
||||
### 커버리지
|
||||
|
||||
| ERP 영역 | SAM 구현 수준 | 비고 |
|
||||
|----------|-------------|------|
|
||||
| **재무회계** | ⭐⭐⭐⭐ (80%) | 매입/매출/입출금/어음/카드/채권 |
|
||||
| **영업관리** | ⭐⭐⭐⭐ (85%) | 견적→수주→생산지시 연동 |
|
||||
| **구매관리** | ⭐⭐⭐ (70%) | 입고 중심, 발주 모듈 약함 |
|
||||
| **재고관리** | ⭐⭐⭐ (65%) | 재고현황 중심, 창고이동 미흡 |
|
||||
| **인사관리** | ⭐⭐⭐⭐ (80%) | 근태/급여/휴가/문서 |
|
||||
| **전자결재** | ⭐⭐⭐ (70%) | 기안/결재/참조 기본 구조 |
|
||||
| **프로젝트** | ⭐⭐⭐⭐⭐ (90%) | 건설 모듈이 매우 정교함 |
|
||||
|
||||
### ERP로서의 강점
|
||||
|
||||
1. **영업-생산 연동**: 수주가 바로 생산지시로 연결되는 구조
|
||||
2. **프로젝트 관리**: 입찰→계약→시공→정산까지 풀 사이클
|
||||
3. **회계 통합**: 매출/매입이 거래처원장과 연동
|
||||
4. **멀티테넌트**: 신규 고객사 온보딩이 빠름
|
||||
|
||||
### ERP로서의 약점
|
||||
|
||||
1. **구매/발주**: 입고 위주, 구매요청→발주→입고 흐름 미흡
|
||||
2. **원가계산**: 제조원가 계산 로직이 명시적이지 않음
|
||||
3. **창고관리**: 다창고, 로케이션 관리 부재
|
||||
4. **BI/분석**: 대시보드는 있으나 심층 분석 약함
|
||||
|
||||
---
|
||||
|
||||
## 3. MES 관점 분석
|
||||
|
||||
### 커버리지
|
||||
|
||||
| MES 영역 | SAM 구현 수준 | 비고 |
|
||||
|----------|-------------|------|
|
||||
| **작업지시** | ⭐⭐⭐⭐ (80%) | 생산지시 생성/관리 |
|
||||
| **작업실적** | ⭐⭐⭐⭐ (80%) | 실적 입력/조회 |
|
||||
| **품질관리** | ⭐⭐⭐⭐⭐ (90%) | 다양한 검사성적서 |
|
||||
| **설비관리** | ⭐ (20%) | 거의 없음 |
|
||||
| **실시간 모니터링** | ⭐⭐⭐ (60%) | 대시보드 있음, PLC 연동 없음 |
|
||||
| **작업자 화면** | ⭐⭐⭐⭐ (75%) | 현장 터치 인터페이스 |
|
||||
| **추적성(Traceability)** | ⭐⭐⭐ (65%) | 로트 추적 기본 구조 |
|
||||
|
||||
### MES로서의 강점
|
||||
|
||||
1. **품질 시스템**: QMS가 상당히 정교함 (6종 검사성적서)
|
||||
2. **작업자 친화적**: 현장용 작업자 화면 별도 존재
|
||||
3. **생산-영업 연결**: 수주 기반 생산이라 주문 추적 용이
|
||||
|
||||
### MES로서의 약점
|
||||
|
||||
1. **설비 연동 없음**: PLC, 바코드 스캐너 등 현장 장비 연동 부재
|
||||
2. **실시간성 부족**: 폴링 기반, 실시간 푸시 아님
|
||||
3. **공정 스케줄링**: 단순 작업지시, APS(고급계획) 없음
|
||||
4. **설비 모니터링**: OEE, 설비 가동률 등 없음
|
||||
|
||||
---
|
||||
|
||||
## 4. 현장 효용성 평가
|
||||
|
||||
### 실제로 잘 맞는 업종
|
||||
|
||||
```
|
||||
✅ 주문생산 제조업 (블라인드, 가구, 인테리어 자재)
|
||||
✅ 설치 서비스 병행 업체
|
||||
✅ 다품종 소량생산
|
||||
✅ 품질 인증 필요 업종 (ISO, KS 등)
|
||||
✅ 직원 50명 이하 중소기업
|
||||
✅ IT 인력이 부족한 회사 (SaaS로 운영부담 최소화)
|
||||
```
|
||||
|
||||
### 맞지 않는 업종
|
||||
|
||||
```
|
||||
❌ 대량생산 (자동차, 반도체) - MES 깊이 부족
|
||||
❌ 연속공정 (화학, 식품) - 배치/레시피 관리 없음
|
||||
❌ 설비 집약 산업 - 설비 연동/모니터링 없음
|
||||
❌ 복잡한 원가계산 필요 업종 - 원가 모듈 약함
|
||||
❌ 대기업 (100명+) - 워크플로우 복잡도 한계
|
||||
```
|
||||
|
||||
### 현장에서의 실제 가치
|
||||
|
||||
| 관점 | 효용 |
|
||||
|------|------|
|
||||
| **경영진** | 수주~매출까지 한눈에, 프로젝트별 손익 파악 |
|
||||
| **영업팀** | 견적→수주→생산현황 실시간 확인 |
|
||||
| **생산팀** | 작업지시 받고 실적 입력, 품질 기록 |
|
||||
| **품질팀** | 검사성적서 발행, 인증심사 대응 |
|
||||
| **경리팀** | 매입/매출/입출금 통합 관리 |
|
||||
| **현장 작업자** | 터치 화면으로 작업 확인/실적 입력 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 경쟁 포지션
|
||||
|
||||
### vs 범용 ERP (더존, 영림원)
|
||||
|
||||
| 항목 | SAM | 범용 ERP |
|
||||
|------|-----|---------|
|
||||
| 제조 특화 | ⭐⭐⭐⭐ | ⭐⭐ |
|
||||
| 건설/시공 | ⭐⭐⭐⭐⭐ | ⭐ |
|
||||
| 회계 깊이 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
| 커스터마이징 | ⭐⭐⭐⭐ (빌더) | ⭐⭐ |
|
||||
| 도입 비용 | 낮음 (SaaS) | 높음 |
|
||||
|
||||
### vs 전문 MES (포스코ICT, 미라콤)
|
||||
|
||||
| 항목 | SAM | 전문 MES |
|
||||
|------|-----|---------|
|
||||
| 설비 연동 | ❌ | ⭐⭐⭐⭐⭐ |
|
||||
| 실시간성 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
| 품질 관리 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
|
||||
| ERP 통합 | ⭐⭐⭐⭐⭐ | ⭐⭐ (별도 연동) |
|
||||
| 도입 기간 | 짧음 | 길음 |
|
||||
|
||||
### SAM의 틈새 (Niche)
|
||||
|
||||
```
|
||||
"전문 MES는 과하고, 범용 ERP는 제조 기능이 부족한"
|
||||
중소 제조+설치 업체를 위한 통합 솔루션
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 빌더 확장 시 기대효과
|
||||
|
||||
현재 품목기준관리 빌더를 다른 영역으로 확장하면:
|
||||
|
||||
| 확장 영역 | 예상 효과 |
|
||||
|----------|----------|
|
||||
| **폼 빌더** (등록/수정) | 신규 업종 대응 시 개발 50% 절감 |
|
||||
| **리스트 빌더** (조회) | 화면 추가/변경 무코딩 가능 |
|
||||
| **문서 빌더** (성적서) | 업종별 양식 빠른 대응 |
|
||||
| **워크플로우 빌더** | 결재/승인 프로세스 설정화 |
|
||||
|
||||
**신규 업체 온보딩 시나리오**:
|
||||
```
|
||||
현재: 요구분석 → 개발 → 테스트 → 배포 (4-8주)
|
||||
목표: 요구분석 → 빌더 설정 → 배포 (1-2주)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 종합 평가
|
||||
|
||||
### SAM의 정체성 한 문장
|
||||
|
||||
> **"주문생산 중소 제조업을 위한 ERP+MES 통합 SaaS로, 생산-품질-영업-회계를 하나로 연결하고, 설치 프로젝트까지 관리하는 올인원 솔루션"**
|
||||
|
||||
### 핵심 차별점
|
||||
|
||||
1. **제조+설치 통합** - 대부분의 시스템이 둘 중 하나만 함
|
||||
2. **품질 시스템 내장** - QMS가 기본 탑재
|
||||
3. **빌더 기반 확장성** - 업종별 커스터마이징 용이
|
||||
4. **SaaS 멀티테넌트** - 도입 부담 최소화
|
||||
|
||||
### 발전 방향 제안
|
||||
|
||||
| 단기 | 중기 | 장기 |
|
||||
|------|------|------|
|
||||
| 빌더 → 리스트까지 확장 | 바코드/QR 스캐닝 | 설비 연동 (IoT) |
|
||||
| 발주 모듈 보강 | 모바일 앱 강화 | AI 수요 예측 |
|
||||
| 원가계산 기본 기능 | 실시간 알림 (WebSocket) | APS 스케줄링 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 시스템 규모 현황
|
||||
|
||||
### 프로젝트 스케일
|
||||
|
||||
- **24개** 주요 기능 모듈
|
||||
- **250+** 페이지
|
||||
- **900+** 컴포넌트 파일
|
||||
- 멀티테넌트 아키텍처
|
||||
- 다국어 지원 (한국어, 영어, 일본어)
|
||||
|
||||
### 모듈별 복잡도
|
||||
|
||||
| 모듈 | 복잡도 | 페이지 수 | 핵심 기능 |
|
||||
|------|--------|----------|----------|
|
||||
| Construction | ⭐⭐⭐⭐⭐ | 57 | 프로젝트 풀 라이프사이클 |
|
||||
| Accounting | ⭐⭐⭐⭐ | 31 | 재무 관리 전체 |
|
||||
| Production | ⭐⭐⭐⭐ | 12 | 실시간 MES 코어 |
|
||||
| Quality | ⭐⭐⭐⭐ | 24 | 다중 검사 QMS |
|
||||
| Master Data | ⭐⭐⭐⭐⭐ | 12 | 동적 폼 템플릿 |
|
||||
| Sales | ⭐⭐⭐ | 20 | 견적→수주 흐름 |
|
||||
| HR | ⭐⭐⭐ | 17 | 직원 라이프사이클 |
|
||||
| Material | ⭐⭐ | 6 | 재고 & 입고 |
|
||||
| Outbound | ⭐⭐ | 7 | 출고 & 배차 |
|
||||
|
||||
---
|
||||
|
||||
## 부록: 기술 스택
|
||||
|
||||
**Frontend:**
|
||||
- Next.js 15 (App Router)
|
||||
- React 18
|
||||
- TypeScript
|
||||
- Tailwind CSS
|
||||
- Radix UI
|
||||
- Zustand
|
||||
|
||||
**Backend:**
|
||||
- PHP Laravel API (별도 코드베이스)
|
||||
- MySQL/MariaDB
|
||||
- JWT 인증
|
||||
- 멀티테넌트 아키텍처
|
||||
|
||||
**인프라:**
|
||||
- HttpOnly 쿠키 보안
|
||||
- 멀티테넌트 데이터 격리
|
||||
- RESTful API 설계
|
||||
@@ -0,0 +1,505 @@
|
||||
# 리스트 페이지 공통화 현황 분석
|
||||
|
||||
> 작성일: 2026-02-05
|
||||
> 목적: 리스트 페이지 반복 패턴 식별 및 공통화 가능성 분석
|
||||
|
||||
---
|
||||
|
||||
## 📊 전체 현황
|
||||
|
||||
| 구분 | 수량 |
|
||||
|------|------|
|
||||
| 총 리스트 페이지 | 37개 |
|
||||
| UniversalListPage 사용 | 15개+ |
|
||||
| IntegratedListTemplateV2 직접 사용 | 5개+ |
|
||||
| 레거시 패턴 | 10개+ |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 템플릿 계층 구조
|
||||
|
||||
```
|
||||
UniversalListPage (최상위 - config 기반)
|
||||
└── IntegratedListTemplateV2 (하위 - props 기반)
|
||||
└── 공통 UI 컴포넌트
|
||||
├── PageLayout
|
||||
├── PageHeader
|
||||
├── StatCards
|
||||
├── DateRangeSelector
|
||||
├── MobileFilter
|
||||
├── ListMobileCard
|
||||
└── Table, Pagination 등
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 리스트 페이지 목록 및 사용 템플릿
|
||||
|
||||
### UniversalListPage 사용 (최신 패턴)
|
||||
|
||||
| 파일 | 도메인 | 특징 |
|
||||
|------|--------|------|
|
||||
| `items/ItemListClient.tsx` | 품목관리 | 외부 훅(useItemList) 사용, 엑셀 업로드/다운로드 |
|
||||
| `pricing/PricingListClient.tsx` | 가격관리 | 외부 훅 사용 |
|
||||
| `production/WorkOrders/WorkOrderList.tsx` | 생산 | 공정 기반 탭, 외부 통계 API |
|
||||
| `outbound/ShipmentManagement/ShipmentList.tsx` | 출고 | 캘린더 통합, 날짜범위 필터 |
|
||||
| `outbound/VehicleDispatchManagement/VehicleDispatchList.tsx` | 배차 | - |
|
||||
| `material/ReceivingManagement/ReceivingList.tsx` | 입고 | - |
|
||||
| `material/StockStatus/StockStatusList.tsx` | 재고 | - |
|
||||
| `customer-center/NoticeManagement/NoticeList.tsx` | 공지사항 | 클라이언트 사이드 필터링 |
|
||||
| `customer-center/EventManagement/EventList.tsx` | 이벤트 | 클라이언트 사이드 필터링 |
|
||||
| `customer-center/InquiryManagement/InquiryList.tsx` | 문의 | - |
|
||||
| `customer-center/FAQManagement/FAQList.tsx` | FAQ | - |
|
||||
| `quality/InspectionManagement/InspectionList.tsx` | 품질검사 | - |
|
||||
| `process-management/ProcessListClient.tsx` | 공정관리 | - |
|
||||
| `pricing-table-management/PricingTableListClient.tsx` | 단가표 | - |
|
||||
| `pricing-distribution/PriceDistributionList.tsx` | 가격배포 | - |
|
||||
|
||||
### 건설 도메인 (UniversalListPage 사용)
|
||||
|
||||
| 파일 | 기능 |
|
||||
|------|------|
|
||||
| `construction/management/ProjectListClient.tsx` | 프로젝트 목록 |
|
||||
| `construction/management/ConstructionManagementListClient.tsx` | 공사관리 목록 |
|
||||
| `construction/contract/ContractListClient.tsx` | 계약 목록 |
|
||||
| `construction/estimates/EstimateListClient.tsx` | 견적 목록 |
|
||||
| `construction/bidding/BiddingListClient.tsx` | 입찰 목록 |
|
||||
| `construction/pricing-management/PricingListClient.tsx` | 단가관리 목록 |
|
||||
| `construction/partners/PartnerListClient.tsx` | 협력사 목록 |
|
||||
| `construction/order-management/OrderManagementListClient.tsx` | 발주관리 목록 |
|
||||
| `construction/site-management/SiteManagementListClient.tsx` | 현장관리 목록 |
|
||||
| `construction/site-briefings/SiteBriefingListClient.tsx` | 현장브리핑 목록 |
|
||||
| `construction/handover-report/HandoverReportListClient.tsx` | 인수인계 목록 |
|
||||
| `construction/issue-management/IssueManagementListClient.tsx` | 이슈관리 목록 |
|
||||
| `construction/structure-review/StructureReviewListClient.tsx` | 구조검토 목록 |
|
||||
| `construction/utility-management/UtilityManagementListClient.tsx` | 유틸리티 목록 |
|
||||
| `construction/worker-status/WorkerStatusListClient.tsx` | 작업자 현황 |
|
||||
| `construction/progress-billing/ProgressBillingManagementListClient.tsx` | 기성관리 목록 |
|
||||
|
||||
### 기타/레거시
|
||||
|
||||
| 파일 | 비고 |
|
||||
|------|------|
|
||||
| `settings/PopupManagement/PopupList.tsx` | 팝업관리 |
|
||||
| `production/WorkResults/WorkResultList.tsx` | 작업실적 |
|
||||
| `quality/PerformanceReportManagement/PerformanceReportList.tsx` | 성과보고서 |
|
||||
| `board/BoardList/BoardListUnified.tsx` | 통합 게시판 |
|
||||
|
||||
---
|
||||
|
||||
## 🔄 반복 패턴 분석
|
||||
|
||||
### 1. Badge 색상 매핑 (매우 반복적)
|
||||
|
||||
각 페이지마다 개별 정의되어 있는 패턴:
|
||||
|
||||
```typescript
|
||||
// ItemListClient.tsx
|
||||
const badges: Record<string, { variant: string; className: string }> = {
|
||||
FG: { variant: 'default', className: 'bg-purple-100 text-purple-700 border-purple-200' },
|
||||
PT: { variant: 'default', className: 'bg-orange-100 text-orange-700 border-orange-200' },
|
||||
// ...
|
||||
};
|
||||
|
||||
// WorkOrderList.tsx
|
||||
const PRIORITY_COLORS: Record<string, string> = {
|
||||
'긴급': 'bg-red-100 text-red-700',
|
||||
'우선': 'bg-orange-100 text-orange-700',
|
||||
'일반': 'bg-gray-100 text-gray-700',
|
||||
};
|
||||
|
||||
// ShipmentList.tsx - types.ts에서 import
|
||||
export const SHIPMENT_STATUS_STYLES: Record<ShipmentStatus, string> = { ... };
|
||||
```
|
||||
|
||||
**현황**:
|
||||
- `src/lib/utils/status-config.ts`에 `createStatusConfig` 유틸 존재
|
||||
- 일부 페이지만 사용 중 (대부분 개별 정의)
|
||||
|
||||
### 2. 상태 라벨 정의 (반복적)
|
||||
|
||||
```typescript
|
||||
// WorkOrderList.tsx - types.ts에서 import
|
||||
export const WORK_ORDER_STATUS_LABELS: Record<WorkOrderStatus, string> = {
|
||||
pending: '대기',
|
||||
in_progress: '진행중',
|
||||
completed: '완료',
|
||||
};
|
||||
|
||||
// ShipmentList.tsx - types.ts에서 import
|
||||
export const SHIPMENT_STATUS_LABELS: Record<ShipmentStatus, string> = { ... };
|
||||
```
|
||||
|
||||
**현황**: 각 도메인 types.ts에서 개별 정의
|
||||
|
||||
### 3. 필터 설정 (filterConfig)
|
||||
|
||||
```typescript
|
||||
// WorkOrderList.tsx
|
||||
const filterConfig: FilterFieldConfig[] = [
|
||||
{
|
||||
key: 'status',
|
||||
label: '상태',
|
||||
type: 'single',
|
||||
options: [
|
||||
{ value: 'waiting', label: '작업대기' },
|
||||
{ value: 'in_progress', label: '진행중' },
|
||||
{ value: 'completed', label: '작업완료' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'priority',
|
||||
label: '우선순위',
|
||||
type: 'single',
|
||||
options: [
|
||||
{ value: 'urgent', label: '긴급' },
|
||||
{ value: 'priority', label: '우선' },
|
||||
{ value: 'normal', label: '일반' },
|
||||
],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
**공통 필터 패턴**:
|
||||
- 상태 필터 (대기/진행/완료)
|
||||
- 우선순위 필터 (긴급/우선/일반)
|
||||
- 유형 필터 (전체/유형1/유형2...)
|
||||
|
||||
### 4. 행 클릭 핸들러 패턴
|
||||
|
||||
```typescript
|
||||
// 모든 페이지에서 동일한 패턴
|
||||
const handleRowClick = useCallback(
|
||||
(item: SomeType) => {
|
||||
router.push(`/ko/${basePath}/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
```
|
||||
|
||||
### 5. 테이블 행 렌더링 (renderTableRow)
|
||||
|
||||
```typescript
|
||||
// 공통 구조
|
||||
<TableRow onClick={() => handleRowClick(item)}>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox checked={isSelected} onCheckedChange={onToggle} />
|
||||
</TableCell>
|
||||
<TableCell className="text-center">{globalIndex}</TableCell>
|
||||
{/* 데이터 컬럼들 */}
|
||||
<TableCell>
|
||||
<Badge className={getStatusStyle(item.status)}>
|
||||
{getStatusLabel(item.status)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 이미 공통화된 것
|
||||
|
||||
| 유틸/컴포넌트 | 위치 | 사용률 |
|
||||
|--------------|------|--------|
|
||||
| `UniversalListPage` | templates/ | 높음 (15개+) |
|
||||
| `IntegratedListTemplateV2` | templates/ | 높음 |
|
||||
| `ListMobileCard`, `InfoField` | organisms/ | 높음 |
|
||||
| `MobileFilter` | molecules/ | 높음 |
|
||||
| `DateRangeSelector` | molecules/ | 높음 |
|
||||
| `StatCards` | organisms/ | 높음 |
|
||||
| `createStatusConfig` | lib/utils/ | **낮음** (일부만 사용) |
|
||||
|
||||
---
|
||||
|
||||
## ❌ 공통화 필요한 것
|
||||
|
||||
### 높은 우선순위 (ROI 높음)
|
||||
|
||||
| 패턴 | 현황 | 공통화 방안 |
|
||||
|------|------|-------------|
|
||||
| **Badge 색상 매핑** | 각 페이지 개별 정의 | `src/lib/utils/badge-styles.ts` 생성 |
|
||||
| **공통 필터 프리셋** | 각 페이지 개별 정의 | `src/lib/constants/filter-presets.ts` 생성 |
|
||||
| **우선순위 색상** | 각 페이지 개별 정의 | 공통 상수로 추출 |
|
||||
|
||||
### 중간 우선순위
|
||||
|
||||
| 패턴 | 현황 | 공통화 방안 |
|
||||
|------|------|-------------|
|
||||
| 상태 라벨 | 도메인별 types.ts | 도메인별 유지 (비즈니스 로직) |
|
||||
| 행 클릭 핸들러 | 각 페이지 개별 | UniversalListPage에서 처리 중 |
|
||||
|
||||
---
|
||||
|
||||
## 📋 공통화 대상 상세
|
||||
|
||||
### 1. Badge 스타일 공통화
|
||||
|
||||
**현재 분산된 위치**:
|
||||
- `items/ItemListClient.tsx` - getItemTypeBadge()
|
||||
- `production/WorkOrders/types.ts` - WORK_ORDER_STATUS_COLORS
|
||||
- `outbound/ShipmentManagement/types.ts` - SHIPMENT_STATUS_STYLES
|
||||
- 기타 각 도메인별 개별 정의
|
||||
|
||||
**이미 존재하는 공통 유틸** (`src/lib/utils/status-config.ts`):
|
||||
```typescript
|
||||
export const BADGE_STYLE_PRESETS: Record<StatusStylePreset, string> = {
|
||||
default: 'bg-gray-100 text-gray-800',
|
||||
success: 'bg-green-100 text-green-800',
|
||||
warning: 'bg-yellow-100 text-yellow-800',
|
||||
destructive: 'bg-red-100 text-red-800',
|
||||
info: 'bg-blue-100 text-blue-800',
|
||||
muted: 'bg-gray-100 text-gray-500',
|
||||
orange: 'bg-orange-100 text-orange-800',
|
||||
purple: 'bg-purple-100 text-purple-800',
|
||||
};
|
||||
```
|
||||
|
||||
**문제**: 존재하지만 대부분의 페이지에서 사용하지 않음
|
||||
|
||||
### 2. 공통 필터 프리셋
|
||||
|
||||
**추출 가능한 공통 필터**:
|
||||
```typescript
|
||||
// 상태 필터 (거의 모든 페이지)
|
||||
export const COMMON_STATUS_FILTER: FilterFieldConfig = {
|
||||
key: 'status',
|
||||
label: '상태',
|
||||
type: 'single',
|
||||
options: [
|
||||
{ value: 'pending', label: '대기' },
|
||||
{ value: 'in_progress', label: '진행중' },
|
||||
{ value: 'completed', label: '완료' },
|
||||
],
|
||||
};
|
||||
|
||||
// 우선순위 필터 (생산, 출고 등)
|
||||
export const COMMON_PRIORITY_FILTER: FilterFieldConfig = {
|
||||
key: 'priority',
|
||||
label: '우선순위',
|
||||
type: 'single',
|
||||
options: [
|
||||
{ value: 'urgent', label: '긴급' },
|
||||
{ value: 'priority', label: '우선' },
|
||||
{ value: 'normal', label: '일반' },
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### 3. 우선순위 색상 통합
|
||||
|
||||
**현재 상태**: 여러 파일에서 동일한 색상 반복
|
||||
```typescript
|
||||
// 긴급: bg-red-100 text-red-700
|
||||
// 우선: bg-orange-100 text-orange-700
|
||||
// 일반: bg-gray-100 text-gray-700
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 권장 액션
|
||||
|
||||
### Phase 1: 즉시 실행 가능
|
||||
|
||||
1. **`createStatusConfig` 사용률 높이기**
|
||||
- 기존 유틸 활용도 확인
|
||||
- 새 페이지 작성 시 필수 사용 권장
|
||||
|
||||
2. **공통 필터 프리셋 파일 생성**
|
||||
- 위치: `src/lib/constants/filter-presets.ts`
|
||||
- 상태/우선순위/유형 필터 템플릿
|
||||
|
||||
3. **우선순위 색상 상수 통합**
|
||||
- 위치: `src/lib/utils/status-config.ts`에 추가
|
||||
|
||||
### Phase 2: 점진적 적용
|
||||
|
||||
1. 신규 페이지는 공통 유틸 필수 사용
|
||||
2. 기존 페이지는 수정 시 점진적 마이그레이션
|
||||
3. 기능 변경 없이 import만 변경
|
||||
|
||||
---
|
||||
|
||||
## 📊 공통화 효과 예측
|
||||
|
||||
| 항목 | Before | After |
|
||||
|------|--------|-------|
|
||||
| Badge 정의 위치 | 37개 파일에 분산 | 1개 파일 (+ import) |
|
||||
| 필터 프리셋 | 각 페이지 개별 | 공통 상수 재사용 |
|
||||
| 색상 변경 시 수정 범위 | 37개 파일 | 1개 파일 |
|
||||
| 신규 페이지 개발 시간 | 기존 페이지 참고 필요 | 공통 유틸 import만 |
|
||||
|
||||
---
|
||||
|
||||
## 📝 결론
|
||||
|
||||
1. **UniversalListPage는 이미 잘 구축됨** - 대부분 리스트가 사용 중
|
||||
2. **Badge/필터 공통화가 주요 개선점** - 반복 코드 제거 가능
|
||||
3. **기존 유틸(`createStatusConfig`) 활용도 낮음** - 홍보/가이드 필요
|
||||
4. **기능 변경 없이 공통화 가능** - 리팩토링 리스크 낮음
|
||||
|
||||
---
|
||||
|
||||
## ✅ 공통화 작업 완료 현황 (2026-02-05)
|
||||
|
||||
### 생성된 파일
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `src/lib/constants/filter-presets.ts` | 공통 필터 프리셋 (상태/우선순위/품목유형 등) |
|
||||
| `claudedocs/guides/badge-commonization-guide.md` | Badge 공통화 사용 가이드 |
|
||||
|
||||
### 수정된 파일
|
||||
|
||||
| 파일 | 변경 내용 |
|
||||
|------|----------|
|
||||
| `src/lib/utils/status-config.ts` | 우선순위/품목유형 설정 추가, 한글 라벨 지원 |
|
||||
| `src/components/production/WorkOrders/WorkOrderList.tsx` | 공통 유틸 적용 (샘플 마이그레이션) |
|
||||
|
||||
### 추가된 공통 유틸
|
||||
|
||||
**filter-presets.ts**:
|
||||
- `COMMON_STATUS_FILTER` - 대기/진행/완료
|
||||
- `WORK_STATUS_FILTER` - 작업대기/진행중/작업완료
|
||||
- `COMMON_PRIORITY_FILTER` - 긴급/우선/일반
|
||||
- `ITEM_TYPE_FILTER` - 품목유형
|
||||
- `createSingleFilter()`, `createMultiFilter()` - 커스텀 필터 생성
|
||||
|
||||
**status-config.ts**:
|
||||
- `getPriorityLabel()`, `getPriorityStyle()` - 우선순위 (한글/영문 모두 지원)
|
||||
- `getItemTypeLabel()`, `getItemTypeStyle()` - 품목유형
|
||||
- `COMMON_STATUS_CONFIG`, `WORK_STATUS_CONFIG` 등 - 미리 정의된 상태 설정
|
||||
|
||||
### 샘플 마이그레이션 결과 (WorkOrderList.tsx)
|
||||
|
||||
**Before**:
|
||||
```tsx
|
||||
// 개별 정의
|
||||
const PRIORITY_COLORS: Record<string, string> = {
|
||||
'긴급': 'bg-red-100 text-red-700',
|
||||
'우선': 'bg-orange-100 text-orange-700',
|
||||
'일반': 'bg-gray-100 text-gray-700',
|
||||
};
|
||||
|
||||
const filterConfig: FilterFieldConfig[] = [
|
||||
{ key: 'status', label: '상태', type: 'single', options: [...] },
|
||||
{ key: 'priority', label: '우선순위', type: 'single', options: [...] },
|
||||
];
|
||||
|
||||
<Badge className={`${PRIORITY_COLORS[item.priorityLabel]} border-0`}>
|
||||
```
|
||||
|
||||
**After**:
|
||||
```tsx
|
||||
// 공통 유틸 사용
|
||||
import { WORK_STATUS_FILTER, COMMON_PRIORITY_FILTER } from '@/lib/constants/filter-presets';
|
||||
import { getPriorityStyle } from '@/lib/utils/status-config';
|
||||
|
||||
const filterConfig = [WORK_STATUS_FILTER, COMMON_PRIORITY_FILTER];
|
||||
|
||||
<Badge className={`${getPriorityStyle(item.priorityLabel)} border-0`}>
|
||||
```
|
||||
|
||||
**효과**:
|
||||
- 코드 라인 20줄 → 3줄
|
||||
- 필터 옵션 중복 정의 제거
|
||||
- 색상 일관성 보장
|
||||
|
||||
---
|
||||
|
||||
## 🔄 추가 마이그레이션 (2026-02-05 업데이트)
|
||||
|
||||
### 완료된 마이그레이션
|
||||
|
||||
| 파일 | 적용 내용 | 효과 |
|
||||
|------|----------|------|
|
||||
| `WorkOrderList.tsx` | WORK_STATUS_FILTER + COMMON_PRIORITY_FILTER + getPriorityStyle | 20줄 → 3줄 |
|
||||
| `ItemListClient.tsx` | getItemTypeStyle (품목유형 Badge) | 17줄 → 4줄 |
|
||||
| `ItemDetailClient.tsx` | getItemTypeStyle (품목유형 Badge) | 17줄 → 4줄 |
|
||||
|
||||
### 마이그레이션 제외 대상 (도메인 특화 설정)
|
||||
|
||||
| 파일 | 제외 사유 |
|
||||
|------|----------|
|
||||
| `PricingListClient.tsx` | 다른 색상 체계 (SM=cyan, BENDING 추가 타입) |
|
||||
| `StockStatus/types.ts` | 레거시 타입 지원 (raw_material, bent_part 등) |
|
||||
| `ShipmentManagement/types.ts` | 다른 우선순위 라벨 (보통/낮음) |
|
||||
| `issue-management/types.ts` | 2단계 우선순위 (긴급/일반만) |
|
||||
| `WipProductionModal.tsx` | 버튼 스타일 우선순위 (Badge 아님) |
|
||||
| `ReceivingList.tsx` | 도메인 특화 상태 (입고대기/입고완료/검사완료) |
|
||||
| HR 페이지들 | 도메인 특화 상태 설정 |
|
||||
| 건설 도메인 페이지들 | 도메인 특화 상태 설정 |
|
||||
|
||||
### 분석 결과 요약
|
||||
|
||||
1. **공통 유틸 적용 완료 페이지**: 3개 (WorkOrderList, ItemListClient, ItemDetailClient)
|
||||
2. **도메인 특화 설정 페이지**: 34개 (개별 유지가 적절)
|
||||
3. **결론**: 대부분의 페이지는 도메인별 특화된 상태/라벨/색상을 사용하며, 이는 비즈니스 로직을 명확히 반영하기 위해 의도된 설계
|
||||
|
||||
### 공통 유틸 권장 사용 시나리오
|
||||
|
||||
1. **신규 리스트 페이지 생성 시**: 표준 패턴(대기/진행/완료, 긴급/우선/일반) 사용
|
||||
2. **품목유형 Badge**: 일관된 색상 적용 필요 시 `getItemTypeStyle` 사용
|
||||
3. **우선순위 Badge**: 표준 3단계(긴급/우선/일반) 사용 시 `getPriorityStyle` 사용
|
||||
|
||||
---
|
||||
|
||||
## 🎨 getPresetStyle 마이그레이션 완료 (2026-02-05 최종)
|
||||
|
||||
### 마이그레이션 완료 파일 (22개)
|
||||
|
||||
| 파일 | 적용 내용 |
|
||||
|------|----------|
|
||||
| `orders/OrderRegistration.tsx` | success, info preset |
|
||||
| `pricing-distribution/PriceDistributionDetail.tsx` | success preset |
|
||||
| `pricing/PricingFormClient.tsx` | purple, info, success preset |
|
||||
| `quality/InspectionManagement/InspectionList.tsx` | success, destructive preset |
|
||||
| `quality/InspectionManagement/InspectionCreate.tsx` | success, destructive preset |
|
||||
| `quality/InspectionManagement/InspectionDetail.tsx` | success, destructive preset |
|
||||
| `accounting/PurchaseManagement/index.tsx` | info preset |
|
||||
| `accounting/PurchaseManagement/PurchaseDetail.tsx` | orange preset (기존) |
|
||||
| `accounting/PurchaseManagement/PurchaseDetailModal.tsx` | orange preset (기존) |
|
||||
| `accounting/VendorManagement/CreditAnalysisModal/CreditAnalysisDocument.tsx` | info preset |
|
||||
| `quotes/QuoteRegistration.tsx` | success preset |
|
||||
| `pricing/PricingHistoryDialog.tsx` | info preset |
|
||||
| `business/construction/management/KanbanColumn.tsx` | info preset |
|
||||
| `business/construction/management/DetailCard.tsx` | warning preset |
|
||||
| `business/construction/management/StageCard.tsx` | warning preset |
|
||||
| `business/construction/management/ProjectCard.tsx` | info preset |
|
||||
| `production/WorkerScreen/WorkCard.tsx` | success, destructive preset |
|
||||
| `production/WorkerScreen/ProcessDetailSection.tsx` | warning preset |
|
||||
| `production/ProductionDashboard/index.tsx` | orange, success preset (기존) |
|
||||
| `items/ItemForm/BOMSection.tsx` | info preset (기존) |
|
||||
| `items/DynamicItemForm/sections/DynamicBOMSection.tsx` | info preset (기존) |
|
||||
| `items/ItemMasterDataManagement/tabs/MasterFieldTab/index.tsx` | info preset |
|
||||
| `customer-center/InquiryManagement/InquiryList.tsx` | warning, success preset (기존) |
|
||||
| `hr/EmployeeManagement/CSVUploadDialog.tsx` | success, destructive preset (기존) |
|
||||
|
||||
### 마이그레이션 제외 파일 (유지)
|
||||
|
||||
| 파일 | 제외 사유 |
|
||||
|------|----------|
|
||||
| `business/MainDashboard.tsx` | CEO 대시보드 - 다양한 데이터 시각화용 고유 색상 (achievement %, overdue days 등) |
|
||||
| `pricing/PricingListClient.tsx` | 도메인 특화 색상 체계 (SM=cyan, BENDING type 등) |
|
||||
| `business/CEODashboard/sections/TodayIssueSection.tsx` | 알림 유형별 고유 색상+아이콘 (notification_type 기반) |
|
||||
| `dev/DevToolbar.tsx` | 개발 도구 (운영 무관) |
|
||||
| `ui/status-badge.tsx` | 이미 status-config.ts 사용 중 |
|
||||
| `items/ItemDetailClient.tsx` | getItemTypeStyle 사용 (도메인 특화) |
|
||||
| `items/ItemListClient.tsx` | getItemTypeStyle 사용 (도메인 특화) |
|
||||
|
||||
### 사용된 Preset 유형 통계
|
||||
|
||||
| Preset | 사용 횟수 | 용도 |
|
||||
|--------|----------|------|
|
||||
| `success` | 15+ | 완료, 일치, 활성, 긍정적 상태 |
|
||||
| `info` | 10+ | 정보성 라벨, 진행 상태, 문서 타입 |
|
||||
| `warning` | 6+ | 진행중, 주의 필요, 선행 생산 |
|
||||
| `destructive` | 5+ | 오류, 불일치, 긴급 |
|
||||
| `orange` | 3+ | 품의서/지출결의서, 지연 |
|
||||
| `purple` | 2+ | 최종 확정, 특수 상태 |
|
||||
|
||||
### 마이그레이션 효과
|
||||
|
||||
1. **코드 일관성**: 22개 파일에서 동일한 유틸리티 함수 사용
|
||||
2. **유지보수성**: 색상 변경 시 `status-config.ts` 한 곳만 수정
|
||||
3. **가독성 향상**: `getPresetStyle('success')` vs `bg-green-100 text-green-700 border-green-200`
|
||||
4. **타입 안전성**: TypeScript로 프리셋 이름 자동완성
|
||||
@@ -0,0 +1,165 @@
|
||||
# SAM ERP 프론트엔드 종합 검수 보고서
|
||||
|
||||
> 작성일: 2026-02-19
|
||||
> 분석 범위: src/ 전체 (1,438개 TS/TSX 파일, ~314K줄)
|
||||
> 분석 방법: 5개 에이전트 병렬 분석 (코드품질, 번들/성능, 에러/UX, 아키텍처, 모바일/보안)
|
||||
|
||||
---
|
||||
|
||||
## 종합 스코어카드
|
||||
|
||||
| 영역 | 점수 | 등급 | 핵심 이슈 |
|
||||
|------|------|------|-----------|
|
||||
| **코드 품질** | 7.5/10 | 🟢 양호 | TS 규율 우수, any 133건/TODO 121건 잔존 |
|
||||
| **번들/성능** | 8.5/10 | 🟢 우수 | 동적 로드 적용, tree-shaking 양호 |
|
||||
| **에러/UX 일관성** | 5.5/10 | 🟡 보통 | 에러바운더리 우수, 로딩UI/접근성 미흡 |
|
||||
| **아키텍처** | 6.5/10 | 🟡 보통 | 순환의존 없음, 상태관리 중복 |
|
||||
| **모바일 대응** | 6/10 | 🟡 보통 | 57% 반응형, 터치영역 미달 |
|
||||
| **보안** | 7/10 | 🟢 양호 | 인증 강함, CSP unsafe 허용 |
|
||||
|
||||
**전체: 6.8/10** — 기능적으로 안정적이나, UX 일관성과 아키텍처 정리에 개선 여지
|
||||
|
||||
---
|
||||
|
||||
## 우선순위별 개선 항목
|
||||
|
||||
### P0: 보안 이슈 (즉시 조치)
|
||||
|
||||
| # | 항목 | 심각도 | 현황 | 조치 |
|
||||
|---|------|--------|------|------|
|
||||
| S-1 | CSP `unsafe-inline`/`unsafe-eval` | 🔴 높음 | middleware.ts에서 허용 중 | nonce 기반으로 전환 |
|
||||
| S-2 | `new Function()` 코드 주입 | 🔴 높음 | ComputedField.tsx에서 사용 | 사용자 입력 검증 추가 또는 safe-eval 대체 |
|
||||
| S-3 | sanitizeHTML 함수 강도 | 🟡 중간 | 5개 파일에서 사용 중 | DOMPurify 사용 여부 확인 |
|
||||
|
||||
### P1: 아키텍처 정리 (1~2주)
|
||||
|
||||
| # | 항목 | 현황 | 개선안 |
|
||||
|---|------|------|--------|
|
||||
| A-1 | **상태관리 중복** | ItemMasterContext + itemStore + useItemMasterStore 3중 | Zustand 하나로 통합 |
|
||||
| A-2 | **테마 중복** | ThemeContext + themeStore 병존 | Zustand로 완전 마이그레이션 |
|
||||
| A-3 | **utils 폴더 중복** | `src/utils/` (2개) + `src/lib/utils/` (11개) 병존 | `src/utils/` → `src/lib/utils/`로 통합 |
|
||||
| A-4 | **상수 산재** | constants/ 1개 파일만, 나머지 각 컴포넌트 내부 하드코딩 | 도메인별 `constants/` 정리 |
|
||||
|
||||
### P2: 코드 품질 (2~3주)
|
||||
|
||||
| # | 항목 | 건수 | 현황 | 조치 |
|
||||
|---|------|------|------|------|
|
||||
| Q-1 | `as any` 타입 캐스트 | 64건 | 주로 form errors 처리 | 제네릭 타입 정의 |
|
||||
| Q-2 | `: any` 타입 선언 | 48건 | API 응답/props 타입 | 인터페이스 정의 |
|
||||
| Q-3 | TODO/FIXME 누적 | 121건 (68파일) | useItemMasterStore 15건 등 | 이슈화 → 점진적 해소 |
|
||||
| Q-4 | God 컴포넌트 | 5개 | ItemMasterContext 2,200줄, MainDashboard 1,400줄 | 단계적 분리 |
|
||||
| Q-5 | 거대 훅 | 1개 | useCEODashboard 37.9KB | stats/charts/timeline 분리 |
|
||||
| Q-6 | `alert()`/`confirm()` 잔존 | 32건 | 15개 alert + 17개 confirm | ConfirmDialog/toast로 교체 |
|
||||
|
||||
### P3: UX 일관성 (3~4주)
|
||||
|
||||
| # | 항목 | 현황 | 목표 |
|
||||
|---|------|------|------|
|
||||
| U-1 | **로딩 UI** | 40+ 페이지에서 `"로딩 중..."` 텍스트만 사용, Skeleton 2개만 | Skeleton 기반 로딩으로 통일 |
|
||||
| U-2 | **접근성 (a11y)** | aria-label 3건, role 9건 | 주요 폼/테이블에 ARIA 추가 |
|
||||
| U-3 | **i18n 사용률** | 인프라 완성(ko/en/ja), 실제 사용 ~5% | 점진적 적용 확대 |
|
||||
| U-4 | **Zod 검증** | 2개 폼만 적용 | 신규 폼 필수, 기존은 유지 |
|
||||
| U-5 | **EmptyState 활용** | 컴포넌트 존재하나 하드코딩 "데이터 없음" 다수 | EmptyState 컴포넌트 통일 |
|
||||
|
||||
### P4: 모바일/성능 (선택)
|
||||
|
||||
| # | 항목 | 현황 | 조치 |
|
||||
|---|------|------|------|
|
||||
| M-1 | **반응형 커버리지** | 57% 페이지 적용 | HR/대시보드 등 미적용 페이지 보강 |
|
||||
| M-2 | **터치 영역** | Checkbox 20x20px (권장 44x44px) | 모바일 터치 타겟 확대 |
|
||||
| M-3 | **html2canvas + dom-to-image** 중복 | 2개 라이브러리 공존 | 하나로 통합 (~50-80KB 절감) |
|
||||
| M-4 | **Tiptap 동적 로딩** | 보드/팝업에서만 사용하나 번들 포함 | next/dynamic 적용 (~80-100KB 절감) |
|
||||
| M-5 | **도메인별 actions.ts 표준화** | accounting만 page-level actions, 나머지는 컴포넌트 내부 | accounting 패턴으로 통일 |
|
||||
|
||||
---
|
||||
|
||||
## 잘 되어있는 점 (유지 사항)
|
||||
|
||||
### 코드 품질
|
||||
- ✅ **TypeScript 규율**: @ts-ignore 0건, @ts-nocheck 1건(레거시)
|
||||
- ✅ **console.log 관리**: 23건만 (16건은 logger 유틸리티)
|
||||
- ✅ **에러 바운더리**: 글로벌 + Protected 레벨 4개, Slack 연동
|
||||
- ✅ **Toast 시스템**: sonner 기반 1,277개 인스턴스 일관 사용
|
||||
|
||||
### 번들/성능
|
||||
- ✅ **XLSX 동적 로드**: 버튼 클릭 시에만 ~400KB 로드
|
||||
- ✅ **대시보드 코드 스플리팅**: ~850KB 초기 번들에서 제외
|
||||
- ✅ **tree-shaking**: `import *` 0건, lodash/moment 미사용
|
||||
- ✅ **Zustand 정규화**: 체계적 상태 + Immer + selector hooks
|
||||
- ✅ **Tailwind v4**: 최신 버전, 효율적 트리셰이킹
|
||||
|
||||
### 아키텍처
|
||||
- ✅ **순환 의존성 없음**: pages→components→ui 단방향
|
||||
- ✅ **API 계층**: buildApiUrl 43개 actions.ts 전면 적용
|
||||
- ✅ **executePaginatedAction**: 14개 파일 표준화
|
||||
|
||||
### 보안
|
||||
- ✅ **Bot 차단**: 25개 패턴 필터링
|
||||
- ✅ **다층 인증**: Bearer Token + Authorization 헤더 + Sanctum + API Key
|
||||
- ✅ **Open Redirect 방지**: 내부 경로 검증
|
||||
- ✅ **환경변수 분리**: NEXT_PUBLIC_ 적절히 사용
|
||||
- ✅ **민감 정보 노출 없음**: console.log에 토큰/비밀번호 출력 0건
|
||||
|
||||
---
|
||||
|
||||
## 주요 파일 참조
|
||||
|
||||
### God 컴포넌트 (분리 대상)
|
||||
- `src/contexts/ItemMasterContext.tsx` (2,200줄)
|
||||
- `src/components/business/MainDashboard.tsx` (1,400줄)
|
||||
- `src/hooks/useCEODashboard.ts` (37.9KB)
|
||||
|
||||
### any 타입 집중 지역
|
||||
- `src/components/items/ItemForm/forms/parts/` (22건)
|
||||
- `src/components/items/ItemMasterDataManagement/` (18건)
|
||||
- `src/components/quotes/LocationDetailPanel.tsx` (10건)
|
||||
|
||||
### 보안 확인 대상
|
||||
- `src/middleware.ts` (CSP 설정)
|
||||
- `src/components/**/ComputedField.tsx` (new Function)
|
||||
- sanitizeHTML 사용 파일 5개 (게시판, 팝업, 고객센터)
|
||||
|
||||
### 상태관리 중복
|
||||
- `src/contexts/ItemMasterContext.tsx` vs `src/stores/itemStore.ts` vs `src/stores/item-master/useItemMasterStore.ts`
|
||||
- `src/contexts/ThemeContext.tsx` vs `src/stores/themeStore.ts`
|
||||
|
||||
---
|
||||
|
||||
## 기존 로드맵과의 관계
|
||||
|
||||
| 기존 항목 | 상태 | 이번 분석 결과 |
|
||||
|-----------|------|---------------|
|
||||
| D-1 God 컴포넌트 분리 | ⏳ 대기 | → P2-Q4로 재확인, 여전히 필요 |
|
||||
| D-2 `as` 타입 캐스트 | 보류 | → P2-Q1/Q2로 133건 확인 (기존 ~200건에서 감소) |
|
||||
| D-6 TODO 102건 | ⏳ 대기 | → P2-Q3으로 121건 확인 (소폭 증가) |
|
||||
| A-2 DataTable 최적화 | ⏳ 대기 | → 에이전트 분석 결과 re-render 위험 낮음 (우선순위 하향) |
|
||||
|
||||
### 신규 발견 항목 (기존 로드맵에 없었던 것)
|
||||
- **S-1~S-3**: 보안 이슈 (CSP, code injection, sanitization)
|
||||
- **A-1~A-2**: 상태관리 3중 중복
|
||||
- **U-1~U-5**: UX 일관성 전반 (로딩/접근성/i18n/빈상태)
|
||||
- **M-3~M-4**: 라이브러리 중복/동적 로딩 기회
|
||||
|
||||
---
|
||||
|
||||
## 실행 로드맵 요약
|
||||
|
||||
```
|
||||
Week 1-2: P0 보안 + P1 아키텍처 정리
|
||||
├── CSP nonce 전환
|
||||
├── ComputedField 보안 패치
|
||||
├── 상태관리 중복 정리 (Context → Zustand)
|
||||
└── utils 폴더 통합
|
||||
|
||||
Week 3-4: P2 코드 품질
|
||||
├── any 타입 정리 (form errors 제네릭)
|
||||
├── alert/confirm → ConfirmDialog 교체
|
||||
└── TODO/FIXME 이슈 정리
|
||||
|
||||
Week 5-6: P3 UX 일관성 (선택)
|
||||
├── Skeleton 로딩 UI 통일
|
||||
├── EmptyState 활용 확대
|
||||
└── 접근성 기본 적용
|
||||
|
||||
이후: P4 모바일/성능 (필요 시)
|
||||
```
|
||||
@@ -0,0 +1,396 @@
|
||||
# SAM ERP 프로젝트 심층분석 종합 보고서
|
||||
> 분석일: 2026-02-23 | 분석 영역: Util 분리 / 컴포넌트 공통화 / Zustand 통합
|
||||
|
||||
---
|
||||
|
||||
## 목차
|
||||
1. [Executive Summary](#1-executive-summary)
|
||||
2. [Util 함수 분리 분석](#2-util-함수-분리-분석)
|
||||
3. [컴포넌트 공통화 분석](#3-컴포넌트-공통화-분석)
|
||||
4. [Zustand 스토어 통합 분석](#4-zustand-스토어-통합-분석)
|
||||
5. [통합 리팩토링 로드맵](#5-통합-리팩토링-로드맵)
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
### 전체 현황 스코어카드
|
||||
|
||||
| 영역 | 현재 수준 | 주요 이슈 | 예상 절감 |
|
||||
|------|----------|----------|----------|
|
||||
| **Util 분리** | 🟡 보통 | 중복 함수 6건, 과대 파일 4개, 인라인 유틸 6패턴 | ~800줄 |
|
||||
| **컴포넌트 공통화** | 🟡 보통 | 중복 다이얼로그 5건, Detail 버전 혼재, 패턴 비일관 | ~1,500줄 |
|
||||
| **Zustand 통합** | 🟢 양호 | Context→Zustand 미전환 3건, 셀렉터 훅 미비 | 리렌더 최적화 |
|
||||
|
||||
### Top 5 우선 조치 항목
|
||||
|
||||
1. 🔴 **AuthContext → Zustand 마이그레이션** (전역 리렌더 제거)
|
||||
2. 🔴 **GenericCRUDDialog 추출** (5개 중복 다이얼로그 통합)
|
||||
3. 🔴 **파일 다운로드 로직 통합** (3곳 중복 → 1곳)
|
||||
4. 🟡 **dashboard/transformers.ts 분할** (1,700줄 → 도메인별 분리)
|
||||
5. 🟡 **Detail/DetailClient/DetailClientV2 정리** (버전 혼재 제거)
|
||||
|
||||
---
|
||||
|
||||
## 2. Util 함수 분리 분석
|
||||
|
||||
### 2.1 현재 유틸 파일 인벤토리
|
||||
|
||||
```
|
||||
src/lib/
|
||||
├── utils.ts (cn, safeJsonParse - 최소)
|
||||
├── formatters.ts (phone, businessNumber, card, account 포맷터)
|
||||
├── print-utils.ts (인쇄 유틸)
|
||||
├── sanitize.ts (데이터 정제)
|
||||
├── error-reporting.ts (에러 리포팅)
|
||||
├── utils/ (13개 파일, ~82KB)
|
||||
│ ├── amount.ts (금액 포맷: 원/만원)
|
||||
│ ├── date.ts (날짜 유틸)
|
||||
│ ├── validation.ts (Zod 스키마 - 725줄 ⚠️)
|
||||
│ ├── excel-download.ts (엑셀 다운로드 - 528줄 ⚠️)
|
||||
│ ├── fileDownload.ts (파일 다운로드)
|
||||
│ ├── export.ts (엑셀 내보내기 - 중복 ⚠️)
|
||||
│ ├── search.ts (검색/필터 파이프라인)
|
||||
│ ├── materialTransform.ts (자재 데이터 변환)
|
||||
│ ├── menuTransform.ts (메뉴 구조 변환)
|
||||
│ ├── menuRefresh.ts (메뉴 새로고침)
|
||||
│ ├── status-config.ts (상태 스타일 설정)
|
||||
│ ├── redirect-error.ts (Next.js 리다이렉트 에러)
|
||||
│ └── locale.ts (로케일 유틸)
|
||||
├── api/ (25개 파일)
|
||||
│ ├── error-handler.ts (API 에러 처리)
|
||||
│ ├── toast-utils.ts (토스트 유틸 - 중복 ⚠️)
|
||||
│ ├── transformers.ts (변환기 - 454줄 ⚠️)
|
||||
│ ├── dashboard/transformers.ts (대시보드 변환 - 1,700줄 🔴)
|
||||
│ ├── execute-server-action.ts
|
||||
│ ├── execute-paginated-action.ts
|
||||
│ └── query-params.ts (buildApiUrl - 표준화 완료)
|
||||
├── permissions/ (3개 파일)
|
||||
├── auth/ (2개 파일)
|
||||
└── cache/ (2개 파일)
|
||||
```
|
||||
|
||||
### 2.2 중복 로직 탐지 (6건)
|
||||
|
||||
#### 🔴 HIGH PRIORITY
|
||||
|
||||
| # | 중복 항목 | 위치 | 상세 |
|
||||
|---|----------|------|------|
|
||||
| 1 | **Blob 다운로드** | `export.ts`, `excel-download.ts`, `fileDownload.ts` | 동일한 `URL.createObjectURL → link.click → revokeObjectURL` 패턴이 3곳에 존재 |
|
||||
| 2 | **날짜 문자열 생성** | `export.ts:58`, `excel-download.ts:78` | `toISOString().slice(0,10).replace(/-/g,'')` 동일 패턴, 시간 정밀도만 다름(초 vs 분) |
|
||||
| 3 | **에러 메시지 포맷** | `error-handler.ts:122`, `toast-utils.ts:106` | `getErrorMessage()` vs `formatApiError()` - 동일 로직 |
|
||||
| 4 | **숫자 포맷팅** | `amount.ts:15`, `formatters.ts:178` | `Intl.NumberFormat` vs regex 기반 - 3가지 접근법 혼재 |
|
||||
|
||||
#### 🟡 MEDIUM PRIORITY
|
||||
|
||||
| # | 중복 항목 | 위치 |
|
||||
|---|----------|------|
|
||||
| 5 | 엑셀 파일명 생성 | `export.ts:54` vs `excel-download.ts:78` |
|
||||
| 6 | 쿼리 파라미터 빌드 | 레거시 `URLSearchParams` 패턴 (마이그레이션 완료 상태) |
|
||||
|
||||
### 2.3 인라인 유틸 추출 후보 (6패턴)
|
||||
|
||||
컴포넌트 내부에 반복적으로 등장하지만 util로 분리되지 않은 패턴:
|
||||
|
||||
| 패턴 | 발견 위치 | 영향 파일 | 추천 위치 |
|
||||
|------|----------|----------|----------|
|
||||
| 월/분기 날짜 범위 계산 | TaxInvoice, HR 페이지들 | 5+ | `lib/utils/dateRange.ts` |
|
||||
| 시간 문자열 포맷팅 | TransactionFormModal, time-picker | 4+ | `lib/utils/timeFormatter.ts` |
|
||||
| 포맷된 숫자 파싱 | VendorManagement, Withdrawal 등 | 8+ | `lib/formatters.ts` 확장 |
|
||||
| 에러 객체→메시지 변환 | attendance/page, employee/page | 3+ | `lib/utils/errorFormatter.ts` |
|
||||
| 배열 합계/카운트 reduce | 대시보드, 주문관리 등 | 6+ | `lib/utils/aggregation.ts` |
|
||||
| 파일 크기 포맷팅 | file-input.tsx | 2 | `lib/utils/fileSizeFormatter.ts` |
|
||||
|
||||
### 2.4 과대 파일 (분할 필요)
|
||||
|
||||
| 파일 | 줄 수 | 문제 | 분할 방안 |
|
||||
|------|-------|------|----------|
|
||||
| 🔴 `api/dashboard/transformers.ts` | **1,700+** | 10+ 도메인 변환 혼재 | `dashboard/transformers/{sales,production,quality,accounting,hr,common}.ts` |
|
||||
| 🟡 `utils/validation.ts` | 725 | 5개 아이템 타입 스키마 혼재 | `validations/{item-master-base,product,part,material,filters}.ts` |
|
||||
| 🟡 `utils/excel-download.ts` | 528 | 다운로드/내보내기/템플릿 혼재 | `{blob-download,excel-export,excel-template}.ts` |
|
||||
| 🟡 `api/transformers.ts` | 454 | 27개 export 함수 | `transformers/{pages,sections,fields,bom,templates,options}.ts` |
|
||||
|
||||
### 2.5 미사용 유틸 (후보)
|
||||
|
||||
| 함수 | 파일 | 상태 |
|
||||
|------|------|------|
|
||||
| `parsePhoneNumber()` | `formatters.ts:36` | import 0건 |
|
||||
| `extractNumbers()` | `formatters.ts:220` | import 0건 |
|
||||
| `formatPersonalNumber()` | `formatters.ts:84` | 실제 사용은 `formatPersonalNumberMasked` |
|
||||
|
||||
---
|
||||
|
||||
## 3. 컴포넌트 공통화 분석
|
||||
|
||||
### 3.1 현재 컴포넌트 계층 구조
|
||||
|
||||
```
|
||||
src/components/
|
||||
├── ui/ (49개 - Radix UI 래퍼)
|
||||
├── atoms/ (3개 - 최소 단위)
|
||||
├── molecules/ (9개 - 복합 폼/표시)
|
||||
├── organisms/ (11개 - 비즈니스 컴포넌트)
|
||||
├── templates/ (2+1개 - UniversalListPage, IntegratedDetailTemplate, IntegratedListTemplateV2)
|
||||
├── accounting/ (18개 도메인 폴더, 100+ 컴포넌트)
|
||||
├── settings/ (12개 도메인 폴더)
|
||||
└── [기타 도메인] (15+ 폴더)
|
||||
```
|
||||
|
||||
### 3.2 중복 컴포넌트 패턴 (핵심 발견)
|
||||
|
||||
#### 🔴 CRITICAL: 단순 CRUD 다이얼로그 중복 (5건)
|
||||
|
||||
거의 동일한 구조: Dialog 래퍼 → 폼 필드 → 유효성 검증 → 제출/취소 버튼
|
||||
|
||||
| 컴포넌트 | 줄 수 | 차이점 |
|
||||
|----------|-------|--------|
|
||||
| `settings/RankManagement/RankDialog.tsx` | 89 | 라벨명만 다름 |
|
||||
| `settings/TitleManagement/TitleDialog.tsx` | 90 | 라벨명만 다름 |
|
||||
| `settings/PermissionManagement/PermissionDialog.tsx` | ~90 | 라벨명만 다름 |
|
||||
| `settings/NotificationSettings/ItemSettingsDialog.tsx` | ~90 | 라벨명만 다름 |
|
||||
| `accounting/VendorManagement/CreditAnalysisModal/` | ~100 | 약간 복잡 |
|
||||
|
||||
**해결안**: `GenericCRUDDialog<T>` 제네릭 컴포넌트 생성
|
||||
```typescript
|
||||
// src/components/molecules/GenericCRUDDialog.tsx
|
||||
interface GenericCRUDDialogProps<T> {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
mode: 'add' | 'edit';
|
||||
title: string;
|
||||
fields: FormFieldDefinition[];
|
||||
data?: T;
|
||||
onSubmit: (data: T) => Promise<void>;
|
||||
}
|
||||
```
|
||||
→ **~400줄 절감**
|
||||
|
||||
#### 🔴 CRITICAL: Detail 파일 버전 혼재
|
||||
|
||||
한 엔티티에 대해 여러 버전의 Detail 파일이 공존:
|
||||
|
||||
| 엔티티 | 파일들 | 문제 |
|
||||
|--------|--------|------|
|
||||
| BadDebt | `BadDebtDetail.tsx`, `BadDebtDetailClientV2.tsx` | V2 마이그레이션 미완 |
|
||||
| Withdrawal | `WithdrawalDetailClientV2.tsx` | ClientV2 접미사 |
|
||||
| Deposit | `DepositDetailClientV2.tsx` | ClientV2 접미사 |
|
||||
| Vendor | `VendorDetail.tsx`, `VendorDetailClient.tsx` | 두 파일 공존 |
|
||||
|
||||
→ **단일 소스로 통합 필요, ~300줄 절감**
|
||||
|
||||
#### 🟡 HIGH: 리스트 페이지 설정 중복
|
||||
|
||||
`UniversalListPage`로 통합은 잘 되어있으나, 설정(config) 코드가 각 페이지에 반복:
|
||||
|
||||
| 반복 요소 | 발견 위치 | 해결안 |
|
||||
|-----------|----------|--------|
|
||||
| 상태 관리 (data, filters, pagination) | Sales, Purchase, Vendor 등 | 설정 파일 분리 |
|
||||
| DateRange 선택기 | 8+ 회계 페이지 | `useDateRange()` 훅 표준화 |
|
||||
| Stats 계산 useMemo | 대부분의 리스트 페이지 | `DataStatsCard<T>` 추출 |
|
||||
|
||||
→ **~500줄 절감**
|
||||
|
||||
### 3.3 재사용률 분석
|
||||
|
||||
#### 높은 재사용 (Good)
|
||||
- **UniversalListPage**: 40+ 페이지 (우수)
|
||||
- **IntegratedDetailTemplate**: 20+ 상세 페이지
|
||||
- **FormField**: 50+ 폼
|
||||
|
||||
#### 활용 부족 (Should Use More)
|
||||
- **SearchableSelectionModal**: 실제 3곳만 사용 → 더 광범위 적용 가능
|
||||
- **StandardDialog**: 존재하지만 단순 다이얼로그들이 미사용
|
||||
- **MobileCard**: 정의되었지만 비일관적 사용
|
||||
|
||||
### 3.4 패턴 비일관성
|
||||
|
||||
| 패턴 | 현재 상태 | 표준화 방향 |
|
||||
|------|----------|------------|
|
||||
| 날짜 범위 선택 | 3가지 방식 혼재 (컴포넌트/훅/인라인) | `useDateRange()` + `<DateRangeSelector />` |
|
||||
| 검색/필터 | 3가지 경쟁 패턴 (A: UniversalListPage, B: 커스텀 useState, C: IntegratedListTemplateV2) | Pattern A로 통일 |
|
||||
| 모달 vs 페이지 | VendorDetail→풀페이지, PurchaseDetail→모달 혼재 | 도메인별 기준 확립 |
|
||||
|
||||
### 3.5 추출 필요 공유 컴포넌트
|
||||
|
||||
| 컴포넌트 | 사용처 | 설명 |
|
||||
|----------|--------|------|
|
||||
| `LineItemsTable<T>` | SalesDetail, PurchaseDetail | 품목 추가/삭제/계산 테이블 (~150줄×2 절감) |
|
||||
| `DataStatsCard<T>` | 회계 리스트 페이지들 | 유연한 통계 표시 카드 |
|
||||
| `DocumentTemplate` | CreditAnalysis, InspectionReport | 인쇄용 문서 래퍼 (헤더/푸터/워터마크) |
|
||||
| `DataTableWithActions` | 대부분의 리스트 | 페이지네이션+선택+액션 통합 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Zustand 스토어 통합 분석
|
||||
|
||||
### 4.1 현재 스토어 인벤토리 (7개)
|
||||
|
||||
| 스토어 | 파일 | 줄 수 | 미들웨어 | 용도 |
|
||||
|--------|------|-------|---------|------|
|
||||
| `useItemMasterStore` | `stores/item-master/useItemMasterStore.ts` | 1,150 | devtools, immer | 품목기준관리 정규화 상태 |
|
||||
| `useMasterDataStore` | `stores/masterDataStore.ts` | 450 | devtools | 동적 폼 설정 캐싱 |
|
||||
| `useMenuStore` | `stores/menuStore.ts` | ~100 | persist | 사이드바/메뉴 상태 |
|
||||
| `useFavoritesStore` | `stores/favoritesStore.ts` | ~100 | persist + custom storage | 즐겨찾기 (최대 10개) |
|
||||
| `useThemeStore` | `stores/themeStore.ts` | ~50 | persist | 테마 (light/dark/senior) |
|
||||
| `useTableColumnStore` | `stores/useTableColumnStore.ts` | ~100 | persist + custom storage | 테이블 컬럼 가시성/너비 |
|
||||
| `useCalendarScheduleStore` | `stores/useCalendarScheduleStore.ts` | ~100 | devtools | 캘린더 일정 연도별 캐싱 |
|
||||
|
||||
### 4.2 핵심 발견: Context → Zustand 미전환 (3건)
|
||||
|
||||
#### 🔴 #1: AuthContext (최우선)
|
||||
|
||||
| 항목 | 현재 | 문제 |
|
||||
|------|------|------|
|
||||
| **위치** | `/src/contexts/AuthContext.tsx` (278줄) | React Context + useState |
|
||||
| **상태** | users[], currentUser, roles, tenants | Provider 리렌더 전파 |
|
||||
| **localStorage** | 수동 동기화 (line 162-190) | Zustand persist가 자동 처리 가능 |
|
||||
| **영향** | 사이드바, 대시보드, 모든 인증 페이지 | 상태 변경 시 전체 앱 리렌더 |
|
||||
|
||||
**전환 방안**:
|
||||
```typescript
|
||||
// /src/stores/authStore.ts
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
devtools((set) => ({
|
||||
currentUser: null,
|
||||
setCurrentUser: (user) => set({ currentUser: user }),
|
||||
// ... 기타 액션
|
||||
})),
|
||||
{ name: 'mes-currentUser' }
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
#### 🟡 #2: ItemMasterContext (중복 제거)
|
||||
|
||||
| 항목 | 현재 | 문제 |
|
||||
|------|------|------|
|
||||
| **Context** | `contexts/ItemMasterContext.tsx` (27,922 토큰) | useState 13개+ 상태 |
|
||||
| **Zustand** | `stores/item-master/useItemMasterStore.ts` (1,150줄) | 유사 데이터 관리 |
|
||||
| **중복** | 양쪽에서 품목 마스터 데이터 관리 | 캐싱/API 레이어 분리 |
|
||||
|
||||
→ **Context를 Zustand 스토어로 통합, Context는 얇은 래퍼로만 유지**
|
||||
|
||||
#### 🟡 #3: PermissionContext
|
||||
|
||||
| 항목 | 현재 | 문제 |
|
||||
|------|------|------|
|
||||
| **위치** | `contexts/PermissionContext.tsx` | 순수 데이터/셀렉터 패턴 |
|
||||
| **적합도** | Zustand 셀렉터 패턴에 완벽 부합 | Provider 불필요 |
|
||||
|
||||
### 4.3 셀렉터 훅 미비 (성능 이슈)
|
||||
|
||||
| 스토어 | 셀렉터 훅 | 문제 |
|
||||
|--------|----------|------|
|
||||
| ✅ `masterDataStore` | `usePageConfig()`, `usePageConfigLoading()` 등 | 양호 |
|
||||
| ❌ `useTableColumnStore` | 없음 - 전체 스토어 구독 | 불필요한 리렌더 |
|
||||
| ❌ `useMenuStore` | 없음 - 전체 스토어 구독 | 사이드바 토글이 모든 구독자 리렌더 |
|
||||
| ❌ `useThemeStore` | 없음 | 경미 |
|
||||
|
||||
**해결 패턴**:
|
||||
```typescript
|
||||
// ✅ 추가 필요
|
||||
export const useTableSettings = (pageId: string) =>
|
||||
useTableColumnStore((state) => state.pageSettings[pageId]);
|
||||
|
||||
export const useMenuActiveId = () =>
|
||||
useMenuStore((state) => state.activeMenu);
|
||||
|
||||
export const useSidebarCollapsed = () =>
|
||||
useMenuStore((state) => state.sidebarCollapsed);
|
||||
```
|
||||
|
||||
### 4.4 Custom Storage 중복
|
||||
|
||||
`favoritesStore`와 `tableColumnStore`에서 동일한 사용자별 localStorage 래퍼가 반복:
|
||||
|
||||
```typescript
|
||||
// 두 파일 모두 동일 패턴 반복:
|
||||
const customStorage = {
|
||||
getItem: (name) => { /* userId 기반 키 생성 */ },
|
||||
setItem: (name, value) => { /* userId 기반 키로 저장 */ },
|
||||
removeItem: (name) => { /* userId 기반 키로 삭제 */ },
|
||||
};
|
||||
```
|
||||
|
||||
**해결안**: `/src/lib/storage/user-scoped-storage.ts` 추출
|
||||
```typescript
|
||||
export function createUserScopedStorage(prefix: string): StateStorage {
|
||||
return { getItem, setItem, removeItem };
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 누락된 스토어 기회
|
||||
|
||||
| 스토어 | 용도 | 현재 상태 |
|
||||
|--------|------|----------|
|
||||
| 🔴 `useUIStore` | 전역 모달/노티/로딩 | 각 컴포넌트에서 로컬 관리 |
|
||||
| 🟡 글로벌 필터 상태 | 리스트 페이지 공통 필터 | useState로 산재 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 통합 리팩토링 로드맵
|
||||
|
||||
### Phase 1: 즉시 (1주)
|
||||
|
||||
| 작업 | 영역 | 영향도 | 난이도 |
|
||||
|------|------|--------|--------|
|
||||
| AuthContext → Zustand 마이그레이션 | Zustand | 🔴 전역 리렌더 제거 | 중 |
|
||||
| GenericCRUDDialog 추출 (5개 다이얼로그 통합) | 컴포넌트 | 🔴 ~400줄 절감 | 저 |
|
||||
| Blob 다운로드 로직 통합 (3곳→1곳) | Util | 🔴 중복 제거 | 저 |
|
||||
| 에러 메시지 포맷 통합 (`formatApiError` 제거) | Util | 🟡 API 레이어 정리 | 저 |
|
||||
| Zustand 셀렉터 훅 추가 (3개 스토어) | Zustand | 🟡 리렌더 최적화 | 저 |
|
||||
|
||||
### Phase 2: 단기 (2~3주)
|
||||
|
||||
| 작업 | 영역 | 영향도 | 난이도 |
|
||||
|------|------|--------|--------|
|
||||
| `dashboard/transformers.ts` 분할 (1,700줄) | Util | 🟡 유지보수성 | 중 |
|
||||
| Detail 파일 버전 정리 (V2 통합) | 컴포넌트 | 🟡 ~300줄 절감 | 중 |
|
||||
| `LineItemsTable<T>` organism 추출 | 컴포넌트 | 🟡 Sales/Purchase 공통화 | 중 |
|
||||
| Custom Storage 유틸 추출 | Zustand | 🟡 DRY | 저 |
|
||||
| 날짜 범위 선택 표준화 | 컴포넌트 | 🟡 패턴 통일 | 중 |
|
||||
|
||||
### Phase 3: 중기 (3~4주)
|
||||
|
||||
| 작업 | 영역 | 영향도 | 난이도 |
|
||||
|------|------|--------|--------|
|
||||
| `validation.ts` 분할 (725줄) | Util | 🟢 유지보수성 | 저 |
|
||||
| ItemMasterContext → Zustand 통합 | Zustand | 🟡 중복 제거 | 고 |
|
||||
| IntegratedListTemplateV2 폐기 | 컴포넌트 | 🟢 레거시 제거 | 중 |
|
||||
| 인라인 유틸 추출 (6패턴) | Util | 🟢 코드 품질 | 저 |
|
||||
| 미사용 유틸 함수 정리 | Util | 🟢 코드 청결 | 저 |
|
||||
|
||||
### Phase 4: 장기 (4주+)
|
||||
|
||||
| 작업 | 영역 | 영향도 | 난이도 |
|
||||
|------|------|--------|--------|
|
||||
| PermissionContext → Zustand | Zustand | 🟢 아키텍처 통일 | 중 |
|
||||
| DocumentTemplate organism 추출 | 컴포넌트 | 🟢 인쇄 공통화 | 중 |
|
||||
| useUIStore 생성 (전역 UI 상태) | Zustand | 🟢 모달/노티 통합 | 중 |
|
||||
| 숫자 포맷팅 API 표준화 | Util | 🟢 일관성 | 저 |
|
||||
|
||||
---
|
||||
|
||||
## 부록: 핵심 파일 참조
|
||||
|
||||
### 리팩토링 대상 (Util)
|
||||
- `/src/lib/utils/export.ts` - 중복 제거 대상
|
||||
- `/src/lib/utils/excel-download.ts` - 분할 대상 (528줄)
|
||||
- `/src/lib/utils/validation.ts` - 분할 대상 (725줄)
|
||||
- `/src/lib/api/dashboard/transformers.ts` - 분할 대상 (1,700줄)
|
||||
- `/src/lib/api/toast-utils.ts` - `formatApiError` 제거 대상
|
||||
|
||||
### 리팩토링 대상 (컴포넌트)
|
||||
- `/src/components/settings/RankManagement/RankDialog.tsx` - GenericCRUDDialog로 대체
|
||||
- `/src/components/settings/TitleManagement/TitleDialog.tsx` - GenericCRUDDialog로 대체
|
||||
- `/src/components/accounting/BadDebtCollection/BadDebtDetailClientV2.tsx` - 버전 통합
|
||||
- `/src/components/accounting/WithdrawalManagement/WithdrawalDetailClientV2.tsx` - 버전 통합
|
||||
- `/src/components/accounting/DepositManagement/DepositDetailClientV2.tsx` - 버전 통합
|
||||
|
||||
### 리팩토링 대상 (Zustand)
|
||||
- `/src/contexts/AuthContext.tsx` → `/src/stores/authStore.ts`
|
||||
- `/src/contexts/ItemMasterContext.tsx` → `/src/stores/item-master/` 통합
|
||||
- `/src/stores/useTableColumnStore.ts` - 셀렉터 훅 추가
|
||||
- `/src/stores/menuStore.ts` - 셀렉터 훅 추가
|
||||
- `/src/stores/favoritesStore.ts` - custom storage 유틸 추출
|
||||
@@ -0,0 +1,176 @@
|
||||
# CEO Dashboard 분석 (기획서 D1.7 기준)
|
||||
|
||||
**기획서**: `SAM_ERP_Storyboard_D1.7_260227.pdf` p33~60
|
||||
**분석일**: 2026-02-27
|
||||
**상태**: 기획서 분석 완료, 구현 대기
|
||||
|
||||
---
|
||||
|
||||
## 1. 전체 구성
|
||||
|
||||
| 구분 | 페이지 | 수량 |
|
||||
|------|--------|------|
|
||||
| 메인 대시보드 섹션 | p33~43 | 20개 |
|
||||
| 상세 모달 | p44~57 | 10개 |
|
||||
| 참고 자료 (계산공식) | p58~60 | 3페이지 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 섹션별 현황 (20개)
|
||||
|
||||
### API 연동 완료 (11개)
|
||||
|
||||
| # | 섹션 | 페이지 | hook | API endpoint |
|
||||
|---|------|--------|------|-------------|
|
||||
| 1 | 오늘의 이슈 | p33 | useTodayIssue | today-issues/summary |
|
||||
| 2 | 자금 현황 | p33-34 | useCEODashboard | daily-report/summary |
|
||||
| 3 | 현황판 | p34 | useStatusBoard | status-board/summary |
|
||||
| 4 | 당월 예상 지출 | p34-35 | useMonthlyExpense | expected-expenses/summary |
|
||||
| 5 | 가지급금 현황 | p35 | useCardManagement | card-transactions/summary + 2개 |
|
||||
| 6 | 접대비 현황 | p35-36 | useEntertainment | entertainment/summary |
|
||||
| 7 | 복리후생비 현황 | p36 | useWelfare | welfare/summary |
|
||||
| 8 | 미수금 현황 | p36 | useReceivable | receivables/summary |
|
||||
| 9 | 채권추심 현황 | p37 | useDebtCollection | bad-debts/summary |
|
||||
| 10 | 부가세 현황 | p37-38 | useVat | vat/summary |
|
||||
| 11 | 캘린더 | p38 | useCalendar | calendar/schedules |
|
||||
|
||||
### Mock 데이터만 (9개) - API 신규 필요
|
||||
|
||||
| # | 섹션 | 페이지 | 필요 데이터 |
|
||||
|---|------|--------|-----------|
|
||||
| 12 | 매출 현황 | p39 | 누적매출, 달성률, YoY, 당월매출 + 차트2 + 테이블 |
|
||||
| 13 | 일별 매출 내역 | p47(설정) | 매출일, 거래처, 매출금액 (🆕 신규 섹션) |
|
||||
| 14 | 매입 현황 | p40 | 누적매입, 미결제, YoY + 차트2 + 테이블 |
|
||||
| 15 | 일별 매입 내역 | p47(설정) | 매입일, 거래처, 매입금액 (🆕 신규 섹션) |
|
||||
| 16 | 생산 현황 | p41 | 공정별(스크린/슬랫/절곡) 집계 + 작업자현황 |
|
||||
| 17 | 출고 현황 | p41 | 예상출고 7일/30일 금액+건수 |
|
||||
| 18 | 미출고 내역 | p42 | 로트번호, 현장명, 수주처, 잔량, 납기일 |
|
||||
| 19 | 시공 현황 | p42 | 진행/완료(7일이내) + 현장카드 |
|
||||
| 20 | 근태 현황 | p43 | 출근/휴가/지각/결근 + 직원테이블 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 🔴 D1.7 핵심 변경사항
|
||||
|
||||
### 카드 구조 변경 (한도관리형 → 리스크감지형)
|
||||
|
||||
| 섹션 | 기존 구현 | D1.7 기획서 |
|
||||
|------|---------|-----------|
|
||||
| **가지급금** | 카드, 가지급금, 법인세예상, 종합세예상 | 카드, 경조사, 상품권, 접대비, 총합계 (5카드) |
|
||||
| **접대비** | 매출, 분기한도, 잔여한도, 사용금액 | **주말/심야, 기피업종, 고액결제, 증빙미비** |
|
||||
| **복리후생비** | 당해한도, 분기한도, 잔여한도, 사용금액 | **비과세한도초과, 사적사용의심, 특정인편중, 항목별한도초과** |
|
||||
|
||||
### 신규 섹션 (2개)
|
||||
- 일별 매출 내역: 항목 설정(p47)에서 별도 ON/OFF
|
||||
- 일별 매입 내역: 항목 설정(p47)에서 별도 ON/OFF
|
||||
|
||||
### 설정 팝업 확장 (p45-47)
|
||||
- 접대비: 한도관리(연간/반기/분기/월), 기업구분(일반법인/중소기업), 고액결제기준금액
|
||||
- 복리후생비: 한도관리, 계산방식(직원당정액 or 연봉총액×비율), 조건부입력필드, 1회결제기준금액
|
||||
|
||||
---
|
||||
|
||||
## 4. 상세 모달 (10개)
|
||||
|
||||
| # | 모달 | 페이지 | 프론트 config | API 상태 |
|
||||
|---|------|--------|-------------|---------|
|
||||
| 1 | 일정 상세 | p44 | ✅ ScheduleDetailModal | ✅ 연동 |
|
||||
| 2 | 항목 설정 | p45-47 | ✅ DashboardSettingsDialog | localStorage |
|
||||
| 3 | 당월 매입 상세 | p48 | ✅ me1 config | ⚠️ 부분연동 |
|
||||
| 4 | 당월 카드 상세 | p49 | ✅ me2 config | ⚠️ 부분연동 |
|
||||
| 5 | 당월 발행어음 상세 | p50 | ✅ me3 config | ⚠️ 부분연동 |
|
||||
| 6 | 당월 지출 예상 상세 | p51 | ✅ me4 config | ⚠️ 부분연동 |
|
||||
| 7 | 가지급금 상세 | p52 | ✅ cm2 config | ⚠️ 구조변경 필요 |
|
||||
| 8 | 접대비 상세 | p53-54 | ✅ et config | ⚠️ 대폭확장 |
|
||||
| 9 | 복리후생비 상세 | p55-56 | ✅ wf config | ⚠️ 대폭확장 |
|
||||
| 10 | 예상 납부세액 상세 | p57 | ✅ vat config | ⚠️ 확장필요 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 필요 API 작업 (16개)
|
||||
|
||||
### 백엔드 API 수정 (6개)
|
||||
|
||||
| # | API | 변경 내용 |
|
||||
|---|-----|---------|
|
||||
| 1 | 가지급금 summary | 카드/경조사/상품권/접대비 분류 집계 |
|
||||
| 2 | 접대비 summary | 리스크 4종 (주말심야/기피업종/고액/증빙미비) - MCC코드 판별 |
|
||||
| 3 | 복리후생비 summary | 리스크 4종 (비과세초과/사적사용/편중/한도초과) |
|
||||
| 4 | 가지급금 detail | 분류별 상세 + AI분류 컬럼 |
|
||||
| 5 | 복리후생비 detail | 계산방식별 + 분기별현황 |
|
||||
| 6 | 부가세 detail | 신고기간별 + 부가세요약 + 미발행/미수취 |
|
||||
|
||||
### 백엔드 API 신규 (10개)
|
||||
|
||||
| # | API | 용도 | 난이도 |
|
||||
|---|-----|------|--------|
|
||||
| 1 | 접대비 detail | 한도계산 + 분기별현황 + 내역테이블 | 상 |
|
||||
| 2 | 매출 현황 summary | 누적/달성률/YoY/당월 + 차트 | 중 |
|
||||
| 3 | 일별 매출 내역 | 매출일, 거래처, 매출금액 | 하 |
|
||||
| 4 | 매입 현황 summary | 누적/미결제/YoY + 차트 | 중 |
|
||||
| 5 | 일별 매입 내역 | 매입일, 거래처, 매입금액 | 하 |
|
||||
| 6 | 생산 현황 | 공정별 집계 + 작업자실적 | 상 |
|
||||
| 7 | 출고 현황 | 7일/30일 예상출고 | 하 |
|
||||
| 8 | 미출고 내역 | 납기기준 미출고 조회 | 하 |
|
||||
| 9 | 시공 현황 | 진행/완료(7일이내) + 카드 | 중 |
|
||||
| 10 | 근태 현황 | 출근/휴가/지각/결근 집계 | 중 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 프론트엔드 작업 (8개)
|
||||
|
||||
| # | 작업 | 대상 |
|
||||
|---|------|------|
|
||||
| 1 | 가지급금 카드 구조 변경 | CardManagementSection |
|
||||
| 2 | 접대비 카드 → 리스크형 | EntertainmentSection |
|
||||
| 3 | 복리후생비 카드 → 리스크형 | WelfareSection |
|
||||
| 4 | 일별 매출 내역 섹션 신규 | 새 컴포넌트 |
|
||||
| 5 | 일별 매입 내역 섹션 신규 | 새 컴포넌트 |
|
||||
| 6 | 항목 설정 팝업 업데이트 | DashboardSettingsDialog |
|
||||
| 7 | 모달 config API 연동 | 각 modalConfigs |
|
||||
| 8 | Mock 섹션 API 연동 | 매출~근태 hook 생성 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 데이터 아키텍처
|
||||
|
||||
대시보드 전용 테이블 없음. 모든 데이터는 각 도메인 페이지 입력 데이터의 실시간 집계.
|
||||
|
||||
### 자금 현황 데이터 조합
|
||||
| 카드 | 출처 |
|
||||
|------|------|
|
||||
| 일일일보 | bank_accounts 잔액 합계 |
|
||||
| 미수금 잔액 | sales 합계 - deposits 합계 |
|
||||
| 미지급금 잔액 | purchases 합계 - payments 합계 |
|
||||
| 당월 예상 지출 | 매입예정 + 카드결제 + 어음만기 합산 |
|
||||
|
||||
### 리스크 감지 로직 (접대비/복리후생비)
|
||||
- MCC 코드 기반 업종 판별 (p60: 유흥업소, 귀금속, 골프장 등)
|
||||
- 체크 규칙: 시간대이상(22~06시), 업종이상, 금액이상(50만원), 빈도이상(월10회)
|
||||
- 사적사용 의심: 토요일 23시 + 유흥주점 + 25만원 → 2개 규칙 해당
|
||||
|
||||
### 캐싱
|
||||
- sam_stat 테이블 5분 캐시 (백엔드 기존 구현)
|
||||
|
||||
---
|
||||
|
||||
## 8. 참고 계산 공식 (p58-60)
|
||||
|
||||
### 가지급금 인정이자
|
||||
- 인정이자율: 4.6% (당좌대출이자율 기준, 매년 고시)
|
||||
- 인정이자 = 가지급금 × 일이자율(연이자율/365) × 경과일수
|
||||
- 법인세 추가: 인정이자 × 0.19
|
||||
- 대표자 소득세 추가: 인정이자 × 0.35
|
||||
|
||||
### 접대비 손금한도
|
||||
- 기본한도: 일반법인 1,200만원/년, 중소기업 3,600만원/년
|
||||
- 수입금액별 추가한도:
|
||||
- 100억 이하: 수입금액 × 0.2%
|
||||
- 100억~500억: 2,000만원 + (수입금액-100억) × 0.1%
|
||||
- 500억 초과: 6,000만원 + (수입금액-500억) × 0.03%
|
||||
|
||||
### 복리후생비 계산
|
||||
- 방식1 (직원당 정액): 직원수 × 월정액 × 12
|
||||
- 방식2 (연봉총액 비율): 연봉총액 × 비율%
|
||||
- 법정 복리후생비: 4대보험 회사부담분
|
||||
- 비과세 항목별 기준: 식대 20만원, 교통비 10만원, 경조사 5만원, 건강검진 월환산, 교육훈련 8만원, 복지포인트 10만원
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,145 @@
|
||||
# masterDataStore 캐시 테넌트 격리 수정
|
||||
|
||||
**작성일**: 2026-01-29
|
||||
**타입**: 버그 수정 (캐시 격리 누락)
|
||||
**관련 문서**: `[REF-2025-11-19] multi-tenancy-implementation.md`
|
||||
|
||||
---
|
||||
|
||||
## 배경
|
||||
|
||||
멀티테넌시 검토 결과, `TenantAwareCache`(`mes-{tenantId}-{key}`)는 테넌트별로 캐시가 격리되어 있지만, `masterDataStore`의 sessionStorage 캐시는 테넌트 구분 없이 `page_config_{pageType}` 키를 사용하고 있었음.
|
||||
|
||||
추가로 `setCurrentTenantId` 액션이 인터페이스에만 선언되어 있고 **구현도, 호출도 없는** dead code 상태였음.
|
||||
|
||||
---
|
||||
|
||||
## 문제
|
||||
|
||||
### 1. 캐시 키에 tenantId 미포함
|
||||
|
||||
```
|
||||
TenantAwareCache: mes-282-itemMasters ← 테넌트 격리됨
|
||||
masterDataStore: page_config_item-master ← 테넌트 격리 안됨
|
||||
```
|
||||
|
||||
### 2. 발생 가능한 시나리오
|
||||
|
||||
```
|
||||
1. 테넌트 282 사용자가 품목관리 접속
|
||||
→ sessionStorage: page_config_item-master = {테넌트282 설정}
|
||||
|
||||
2. 세션 내 테넌트 500으로 전환 (로그아웃 없이)
|
||||
→ clearTenantCache()는 mes-282-* 만 삭제
|
||||
→ page_config_item-master 는 삭제되지 않음
|
||||
|
||||
3. 테넌트 500 사용자에게 테넌트 282의 페이지 설정이 노출
|
||||
```
|
||||
|
||||
### 3. setCurrentTenantId 미구현
|
||||
|
||||
```typescript
|
||||
// 인터페이스에 선언만 있고 구현 없음
|
||||
interface MasterDataStore {
|
||||
currentTenantId: number | null; // ← initialState에도 누락
|
||||
setCurrentTenantId: (tenantId: number | null) => void; // ← 구현 없음
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 수정 내역
|
||||
|
||||
### masterDataStore.ts
|
||||
|
||||
| 영역 | Before | After |
|
||||
|------|--------|-------|
|
||||
| initialState | `currentTenantId` 누락 | `currentTenantId: null` 추가 |
|
||||
| 캐시 키 포맷 | `page_config_{pageType}` | `page_config_{tenantId}_{pageType}` |
|
||||
| setCurrentTenantId | 인터페이스만 선언 | 구현 추가 |
|
||||
| fetchPageConfig | tenantId 미사용 | `currentTenantId`를 캐시 함수에 전달 |
|
||||
| invalidateConfig | tenantId 미사용 | `currentTenantId` 기반 삭제 |
|
||||
| invalidateAllConfigs | tenantId 미사용 | `currentTenantId` 기반 삭제 |
|
||||
| reset() | pageType 목록 순회 삭제 | `page_config_` 프리픽스 기반 전체 삭제 |
|
||||
|
||||
#### 핵심 변경: 캐시 키 생성 함수 추가
|
||||
|
||||
```typescript
|
||||
function getStorageKey(tenantId: number | null, pageType: PageType): string {
|
||||
return tenantId != null
|
||||
? `${STORAGE_PREFIX}${tenantId}_${pageType}` // page_config_282_item-master
|
||||
: `${STORAGE_PREFIX}${pageType}`; // page_config_item-master (하위 호환)
|
||||
}
|
||||
```
|
||||
|
||||
#### 핵심 변경: reset()을 프리픽스 기반으로 변경
|
||||
|
||||
```typescript
|
||||
// Before: 고정된 pageType 목록으로 삭제 (tenantId 포함 키를 찾지 못함)
|
||||
pageTypes.forEach((pt) => removeConfigFromSessionStorage(pt));
|
||||
|
||||
// After: page_config_ 프리픽스로 모든 테넌트 캐시 일괄 삭제
|
||||
Object.keys(window.sessionStorage).forEach(key => {
|
||||
if (key.startsWith(STORAGE_PREFIX)) {
|
||||
window.sessionStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### AuthContext.tsx
|
||||
|
||||
| 영역 | Before | After |
|
||||
|------|--------|-------|
|
||||
| import | - | `useMasterDataStore` 추가 |
|
||||
| tenantId 동기화 | 없음 | `currentUser.tenant.id` 변경 시 `setCurrentTenantId()` 호출 |
|
||||
| clearTenantCache | `mes-{tenantId}-*` 만 삭제 | `mes-{tenantId}-*` + `page_config_{tenantId}_*` 삭제 |
|
||||
|
||||
#### 핵심 변경: tenantId 동기화 useEffect
|
||||
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
const tenantId = currentUser?.tenant?.id ?? null;
|
||||
useMasterDataStore.getState().setCurrentTenantId(tenantId);
|
||||
}, [currentUser?.tenant?.id]);
|
||||
```
|
||||
|
||||
#### 핵심 변경: clearTenantCache 범위 확장
|
||||
|
||||
```typescript
|
||||
const tenantAwarePrefix = `mes-${tenantId}-`;
|
||||
const pageConfigPrefix = `page_config_${tenantId}_`;
|
||||
|
||||
Object.keys(sessionStorage).forEach(key => {
|
||||
if (key.startsWith(tenantAwarePrefix) || key.startsWith(pageConfigPrefix)) {
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 하위 호환
|
||||
|
||||
| 항목 | 영향 |
|
||||
|------|------|
|
||||
| 기존 캐시 키 | `page_config_item-master` → 키 불일치로 miss → API 재요청 → 새 포맷으로 자동 전환 |
|
||||
| logout.ts | `page_config_` 프리픽스 매칭이 새 키 포맷(`page_config_282_item-master`)도 커버 |
|
||||
| sessionStorage TTL | 10분 만료이므로 기존 키는 자연 소멸 |
|
||||
| tenantId가 null인 경우 | 기존 포맷(`page_config_{pageType}`) 유지하여 동작 보장 |
|
||||
|
||||
---
|
||||
|
||||
## 효과
|
||||
|
||||
1. **세션 내 테넌트 전환 시 캐시 누수 차단**: `clearTenantCache`가 `page_config_{tenantId}_*`까지 삭제
|
||||
2. **캐시 패턴 일관성**: TenantAwareCache(`mes-{tenantId}-`)와 masterDataStore(`page_config_{tenantId}_`) 모두 테넌트 격리
|
||||
3. **dead code 해소**: `currentTenantId` 필드와 `setCurrentTenantId` 액션이 실제로 동작
|
||||
|
||||
---
|
||||
|
||||
## 관련 파일
|
||||
|
||||
- `src/stores/masterDataStore.ts` - 캐시 키 변경, setCurrentTenantId 구현
|
||||
- `src/contexts/AuthContext.tsx` - tenantId 동기화, clearTenantCache 범위 확장
|
||||
- `src/lib/auth/logout.ts` - 기존 `page_config_` 프리픽스 매칭 (변경 없음, 호환 확인)
|
||||
- `src/lib/cache/TenantAwareCache.ts` - 참고 (기존 정상 동작)
|
||||
153
claudedocs/architecture/[GUIDE] component-tier-definition.md
Normal file
153
claudedocs/architecture/[GUIDE] component-tier-definition.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# Component Tier 정의
|
||||
|
||||
> SAM 프로젝트의 컴포넌트 계층(tier) 기준 정의.
|
||||
> 새 컴포넌트 작성 시 어디에 배치할지 판단하는 기준 문서.
|
||||
|
||||
## Tier 구조 요약
|
||||
|
||||
```
|
||||
ui 원시 빌딩블록 (HTML 래퍼, 단일 기능)
|
||||
↓ 조합
|
||||
atoms 최소 단위 UI 조각 (ui 1~2개 조합)
|
||||
↓ 조합
|
||||
molecules 의미 있는 UI 패턴 (atoms/ui 여러 개 조합)
|
||||
↓ 조합
|
||||
organisms 페이지 섹션 단위 (molecules/atoms 조합, 레이아웃 포함)
|
||||
↓ 사용
|
||||
domain 도메인별 비즈니스 컴포넌트 (organisms/molecules 사용)
|
||||
```
|
||||
|
||||
## Tier별 정의
|
||||
|
||||
### ui (원시 빌딩블록)
|
||||
| 항목 | 기준 |
|
||||
|------|------|
|
||||
| 위치 | `src/components/ui/` |
|
||||
| 역할 | HTML 요소를 감싼 최소 단위. 스타일링 + 접근성만 담당 |
|
||||
| 특징 | 비즈니스 로직 없음, 범용적, Radix UI 래퍼 포함 |
|
||||
| 예시 | Button, Input, Select, Badge, Dialog, DatePicker, EmptyState |
|
||||
| 판단 기준 | "이 컴포넌트가 다른 프로젝트에 그대로 복사해도 동작하는가?" → Yes면 ui |
|
||||
|
||||
### atoms (최소 UI 조각)
|
||||
| 항목 | 기준 |
|
||||
|------|------|
|
||||
| 위치 | `src/components/atoms/` |
|
||||
| 역할 | ui 1~2개를 조합한 작은 패턴. 단일 목적 |
|
||||
| 특징 | props 2~5개, 상태 관리 최소 |
|
||||
| 예시 | BadgeSm, TabChip, ScrollableButtonGroup |
|
||||
| 판단 기준 | "ui 하나로는 부족하지만, 독립적인 의미 단위인가?" → Yes면 atoms |
|
||||
|
||||
### molecules (의미 있는 UI 패턴)
|
||||
| 항목 | 기준 |
|
||||
|------|------|
|
||||
| 위치 | `src/components/molecules/` |
|
||||
| 역할 | atoms/ui 여러 개를 조합하여 하나의 기능 패턴을 구성 |
|
||||
| 특징 | Label + Input + Error 같은 조합, 내부 상태 가능 |
|
||||
| 예시 | FormField, StatusBadge, DateRangeSelector, StandardDialog, TableActions |
|
||||
| 판단 기준 | "여러 ui/atoms의 조합이고, 재사용 가능한 패턴인가?" → Yes면 molecules |
|
||||
|
||||
### organisms (페이지 섹션)
|
||||
| 항목 | 기준 |
|
||||
|------|------|
|
||||
| 위치 | `src/components/organisms/` |
|
||||
| 역할 | 페이지의 독립적인 섹션. molecules/atoms를 조합하여 레이아웃 포함 |
|
||||
| 특징 | 데이터 테이블, 검색 필터, 폼 섹션 등 페이지 구성 단위 |
|
||||
| 예시 | DataTable, PageHeader, StatCards, FormSection, SearchableSelectionModal |
|
||||
| 판단 기준 | "페이지에서 하나의 영역으로 독립 가능한가?" → Yes면 organisms |
|
||||
|
||||
### common (공용 페이지/레이아웃)
|
||||
| 항목 | 기준 |
|
||||
|------|------|
|
||||
| 위치 | `src/components/common/` |
|
||||
| 역할 | 에러 페이지, 권한 없음 페이지 등 전역 공통 화면 |
|
||||
| 특징 | 라우터 사용, 전체 페이지 레이아웃 |
|
||||
| 예시 | AccessDenied, EmptyPage, ServerErrorPage |
|
||||
| 판단 기준 | "전체 화면을 차지하는 공통 페이지인가?" → Yes면 common |
|
||||
|
||||
### layout (레이아웃 구조)
|
||||
| 항목 | 기준 |
|
||||
|------|------|
|
||||
| 위치 | `src/components/layout/` |
|
||||
| 역할 | 앱 전체 레이아웃 골격 (사이드바, 헤더, 네비게이션) |
|
||||
| 예시 | AuthenticatedLayout, Sidebar, TopNav |
|
||||
|
||||
### dev (개발 도구)
|
||||
| 항목 | 기준 |
|
||||
|------|------|
|
||||
| 위치 | `src/components/dev/` |
|
||||
| 역할 | 개발 환경 전용 도구 (프로덕션 미포함) |
|
||||
| 예시 | DevToolbar |
|
||||
|
||||
### domain (도메인 비즈니스)
|
||||
| 항목 | 기준 |
|
||||
|------|------|
|
||||
| 위치 | `src/components/{도메인명}/` (hr, sales, accounting 등) |
|
||||
| 역할 | 특정 도메인의 비즈니스 로직이 포함된 컴포넌트 |
|
||||
| 특징 | API 호출, 도메인 타입, 비즈니스 규칙 포함 |
|
||||
| 예시 | EmployeeManagement, OrderRegistration, BillDetail |
|
||||
| 판단 기준 | "특정 도메인에서만 사용되는가?" → Yes면 domain |
|
||||
|
||||
## 자주 혼동되는 케이스
|
||||
|
||||
| 상황 | 올바른 tier | 이유 |
|
||||
|------|-------------|------|
|
||||
| EmptyState (프리셋/variant 있음) | **ui** | 범용 빌딩블록, 비즈니스 로직 없음 |
|
||||
| StatusBadge (icon/dot/색상 커스텀) | **molecules** | Badge + BadgeSm 조합, DataTable 연동 |
|
||||
| ConfirmDialog (삭제/저장 확인) | **ui** | AlertDialog 래퍼, 범용적 |
|
||||
| StandardDialog (범용 컨테이너) | **molecules** | Dialog + Header + Footer 조합 패턴 |
|
||||
| DataTable (정렬/페이지네이션/선택) | **organisms** | 페이지 섹션 단위, 다수 하위 컴포넌트 |
|
||||
| SearchableSelectionModal | **organisms** | 검색+선택 완결 기능, 독립 섹션 |
|
||||
|
||||
## 중복 방지 규칙
|
||||
|
||||
1. **새 컴포넌트 작성 전**: 같은 이름/기능이 다른 tier에 이미 있는지 확인
|
||||
2. **ui에 이미 있으면**: molecules/organisms에 동일 컴포넌트 만들지 않음. 필요하면 ui를 확장
|
||||
3. **re-export 허용**: organisms/index.ts에서 ui 컴포넌트를 re-export 가능 (편의성)
|
||||
4. **확인(Confirm) 다이얼로그**: `ui/confirm-dialog.tsx` 하나만 사용 (52개 파일 사용 중)
|
||||
|
||||
## StatusBadge 역할 구분
|
||||
|
||||
이름이 같지만 tier와 용도가 다른 두 컴포넌트. **둘 다 유지**.
|
||||
|
||||
### `ui/status-badge.tsx` — 범용 상태 배지
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| import | `import { StatusBadge } from '@/components/ui/status-badge'` |
|
||||
| 용도 | `createStatusConfig`와 연동하는 **config 기반** 상태 표시 |
|
||||
| API | `children` 또는 `status + config` 자동 라벨/스타일 |
|
||||
| 특화 기능 | `mode` (badge/text), `ConfiguredStatusBadge` 제네릭 |
|
||||
| 사용 예시 | 템플릿/유틸과 연동하는 범용 상태 표시 |
|
||||
|
||||
```tsx
|
||||
// config 기반 사용
|
||||
<StatusBadge status="pending" config={APPROVAL_STATUS_CONFIG} />
|
||||
|
||||
// children 기반 사용
|
||||
<StatusBadge variant="success">완료</StatusBadge>
|
||||
```
|
||||
|
||||
### `molecules/StatusBadge.tsx` — DataTable 특화 배지
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| import | `import { StatusBadge } from '@/components/molecules/StatusBadge'` |
|
||||
| 용도 | DataTable 셀에서 상태를 **아이콘/도트와 함께** 표시 |
|
||||
| API | `label` 필수, `variant`로 색상 지정 |
|
||||
| 특화 기능 | `icon` (LucideIcon), `showDot`, 커스텀 `bgColor/textColor/borderColor` |
|
||||
| 기반 | Badge + BadgeSm 조합 (size="sm"일 때 BadgeSm으로 자동 전환) |
|
||||
|
||||
```tsx
|
||||
// DataTable 셀 렌더링
|
||||
<StatusBadge label="승인완료" variant="success" showDot />
|
||||
<StatusBadge label="긴급" variant="danger" icon={AlertCircle} />
|
||||
```
|
||||
|
||||
### 선택 기준
|
||||
|
||||
| 상황 | 사용할 컴포넌트 |
|
||||
|------|----------------|
|
||||
| `createStatusConfig` 결과와 연동 | **ui** StatusBadge |
|
||||
| DataTable 컬럼 셀 렌더링 | **molecules** StatusBadge |
|
||||
| 아이콘이나 도트가 필요한 배지 | **molecules** StatusBadge |
|
||||
| 단순 텍스트 상태 표시 (badge/text 모드) | **ui** StatusBadge |
|
||||
@@ -0,0 +1,349 @@
|
||||
# 입력폼 공통 컴포넌트화 구현 계획서
|
||||
|
||||
**작성일**: 2026-01-21
|
||||
**작성자**: Claude Code
|
||||
**상태**: ✅ Phase 1-3 VendorDetail 적용 완료
|
||||
**최종 수정**: 2026-01-21
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요
|
||||
|
||||
### 1.1 목적
|
||||
- 숫자 입력필드의 선행 0(leading zero) 문제 해결
|
||||
- 금액/수량 입력 시 천단위 콤마 및 포맷팅 일관성 확보
|
||||
- 전화번호, 사업자번호, 주민번호 등 포맷팅이 필요한 입력필드 공통화
|
||||
- 소수점 입력이 필요한 필드 지원 (비율, 환율 등)
|
||||
|
||||
### 1.2 현재 문제점
|
||||
| 문제 | 현상 | 영향 범위 |
|
||||
|------|------|----------|
|
||||
| 숫자 입력 leading zero | `01`, `001` 등 표시 | 전체 숫자 입력 |
|
||||
| 금액 포맷팅 불일치 | 콤마 처리 제각각 | **147개 파일** |
|
||||
| 전화번호 포맷팅 없음 | `01012341234` 그대로 표시 | 거래처, 직원 관리 |
|
||||
| 사업자번호 포맷팅 없음 | `1234567890` 그대로 표시 | 거래처 관리 |
|
||||
| Number 타입 일관성 | string/number 혼용 | 타입 에러 가능성 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 구현 우선순위
|
||||
|
||||
### 🔴 Phase 1: 핵심 숫자 입력 (최우선)
|
||||
| 순서 | 컴포넌트 | 용도 | 영향 범위 |
|
||||
|------|---------|------|----------|
|
||||
| 1 | **NumberInput** | 범용 숫자 입력 (leading zero 해결) | 전체 |
|
||||
| 2 | **CurrencyInput** | 금액 입력 (₩, 천단위 콤마) | 147개 파일 |
|
||||
| 3 | **QuantityInput** | 수량 입력 (정수, 최소값 0) | 재고/주문 |
|
||||
|
||||
### 🟠 Phase 2: 포맷팅 입력 (완료)
|
||||
| 순서 | 컴포넌트 | 용도 | 상태 |
|
||||
|------|---------|------|------|
|
||||
| 4 | **PhoneInput** | 전화번호 자동 하이픈 | ✅ 완료 |
|
||||
| 5 | **BusinessNumberInput** | 사업자번호 포맷팅 | ✅ 완료 |
|
||||
| 6 | **PersonalNumberInput** | 주민번호 포맷팅/마스킹 | ✅ 완료 |
|
||||
|
||||
### 🟢 Phase 3: 통합 및 확장
|
||||
| 순서 | 작업 | 설명 |
|
||||
|------|------|------|
|
||||
| 7 | ui/index.ts export | 새 컴포넌트 내보내기 |
|
||||
| 8 | FormField 확장 | 새 타입 지원 추가 |
|
||||
| 9 | 실사용 적용 테스트 | VendorDetail 등 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 생성/수정 파일 목록
|
||||
|
||||
### 3.1 새로 생성한 파일
|
||||
|
||||
```
|
||||
src/
|
||||
├── lib/
|
||||
│ └── formatters.ts ✅ 완료
|
||||
├── components/
|
||||
│ └── ui/
|
||||
│ ├── phone-input.tsx ✅ 완료
|
||||
│ ├── business-number-input.tsx ✅ 완료
|
||||
│ ├── personal-number-input.tsx ✅ 완료
|
||||
│ ├── number-input.tsx ✅ 완료
|
||||
│ ├── currency-input.tsx ✅ 완료
|
||||
│ └── quantity-input.tsx ✅ 완료
|
||||
```
|
||||
|
||||
### 3.2 수정한 파일
|
||||
|
||||
| 파일 | 수정 내용 | 상태 |
|
||||
|------|----------|------|
|
||||
| `src/components/molecules/FormField.tsx` | 새 타입 지원 추가 (phone, businessNumber, personalNumber, currency, quantity) | ✅ 완료 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 컴포넌트 상세 설계
|
||||
|
||||
### 4.1 NumberInput (범용 숫자 입력)
|
||||
|
||||
```typescript
|
||||
interface NumberInputProps {
|
||||
value: number | string | undefined;
|
||||
onChange: (value: number | undefined) => void;
|
||||
|
||||
// 포맷 옵션
|
||||
allowDecimal?: boolean; // 소수점 허용 (기본: false)
|
||||
decimalPlaces?: number; // 소수점 자릿수 제한
|
||||
allowNegative?: boolean; // 음수 허용 (기본: false)
|
||||
useComma?: boolean; // 천단위 콤마 (기본: false)
|
||||
|
||||
// 범위 제한
|
||||
min?: number;
|
||||
max?: number;
|
||||
|
||||
// 표시 옵션
|
||||
suffix?: string; // 접미사 (원, 개, % 등)
|
||||
allowEmpty?: boolean; // 빈 값 허용 (기본: true)
|
||||
}
|
||||
```
|
||||
|
||||
**사용 예시**:
|
||||
```tsx
|
||||
// 기본 정수 입력
|
||||
<NumberInput value={qty} onChange={setQty} />
|
||||
|
||||
// 소수점 2자리 (비율, 환율)
|
||||
<NumberInput value={rate} onChange={setRate} allowDecimal decimalPlaces={2} />
|
||||
|
||||
// 퍼센트 입력 (0-100 제한)
|
||||
<NumberInput value={percent} onChange={setPercent} min={0} max={100} suffix="%" />
|
||||
|
||||
// 음수 허용 (재고 조정)
|
||||
<NumberInput value={adjust} onChange={setAdjust} allowNegative />
|
||||
```
|
||||
|
||||
### 4.2 CurrencyInput (금액 입력)
|
||||
|
||||
```typescript
|
||||
interface CurrencyInputProps {
|
||||
value: number | undefined;
|
||||
onChange: (value: number | undefined) => void;
|
||||
|
||||
currency?: '₩' | '$' | '¥'; // 통화 기호 (기본: ₩)
|
||||
showCurrency?: boolean; // 통화 기호 표시 (기본: true)
|
||||
allowNegative?: boolean; // 음수 허용 (기본: false)
|
||||
}
|
||||
```
|
||||
|
||||
**특징**:
|
||||
- 항상 천단위 콤마 표시
|
||||
- 정수만 허용 (원 단위)
|
||||
- 포커스 해제 시 통화 기호 표시
|
||||
|
||||
### 4.3 QuantityInput (수량 입력)
|
||||
|
||||
```typescript
|
||||
interface QuantityInputProps {
|
||||
value: number | undefined;
|
||||
onChange: (value: number | undefined) => void;
|
||||
|
||||
min?: number; // 최소값 (기본: 0)
|
||||
max?: number; // 최대값
|
||||
step?: number; // 증감 단위 (기본: 1)
|
||||
showButtons?: boolean; // +/- 버튼 표시
|
||||
suffix?: string; // 단위 (개, EA, 박스 등)
|
||||
}
|
||||
```
|
||||
|
||||
**특징**:
|
||||
- 정수만 허용
|
||||
- 기본 최소값 0
|
||||
- 선택적 +/- 버튼
|
||||
|
||||
### 4.4 PhoneInput ✅ 완료
|
||||
|
||||
```typescript
|
||||
interface PhoneInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void; // 숫자만 반환
|
||||
error?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 BusinessNumberInput ✅ 완료
|
||||
|
||||
```typescript
|
||||
interface BusinessNumberInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
showValidation?: boolean; // 유효성 검사 아이콘
|
||||
error?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.6 PersonalNumberInput ✅ 완료
|
||||
|
||||
```typescript
|
||||
interface PersonalNumberInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
maskBack?: boolean; // 뒷자리 마스킹
|
||||
error?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 검수 계획서
|
||||
|
||||
### 5.1 NumberInput 테스트
|
||||
|
||||
| 테스트 항목 | 입력 | 기대 결과 |
|
||||
|------------|------|----------|
|
||||
| Leading zero 제거 | `01` | 표시: `1`, 값: `1` |
|
||||
| Leading zero 제거 | `007` | 표시: `7`, 값: `7` |
|
||||
| 소수점 (허용시) | `3.14` | 표시: `3.14`, 값: `3.14` |
|
||||
| 소수점 자릿수 제한 | `3.14159` (2자리) | 표시: `3.14`, 값: `3.14` |
|
||||
| 음수 (허용시) | `-100` | 표시: `-100`, 값: `-100` |
|
||||
| 콤마 표시 | `1000000` | 표시: `1,000,000`, 값: `1000000` |
|
||||
| 범위 제한 (max:100) | `150` | 값: `100` (제한) |
|
||||
| 빈 값 | `` | 값: `undefined` |
|
||||
| 문자 입력 차단 | `abc` | 입력 안됨 |
|
||||
|
||||
### 5.2 CurrencyInput 테스트
|
||||
|
||||
| 테스트 항목 | 입력 | 기대 결과 |
|
||||
|------------|------|----------|
|
||||
| 기본 입력 | `50000` | 표시: `50,000`, 값: `50000` |
|
||||
| 통화 기호 | `50000` (blur) | 표시: `₩50,000` |
|
||||
| 소수점 차단 | `100.5` | 표시: `100`, 값: `100` |
|
||||
| 대용량 | `1000000000` | 표시: `1,000,000,000` |
|
||||
|
||||
### 5.3 QuantityInput 테스트
|
||||
|
||||
| 테스트 항목 | 입력 | 기대 결과 |
|
||||
|------------|------|----------|
|
||||
| 기본 입력 | `10` | 표시: `10`, 값: `10` |
|
||||
| 음수 차단 | `-5` | 값: `0` (최소값) |
|
||||
| 소수점 차단 | `10.5` | 표시: `10`, 값: `10` |
|
||||
| +/- 버튼 | 클릭 | 1씩 증감 |
|
||||
|
||||
### 5.4 실사용 테스트 페이지
|
||||
|
||||
| 페이지 | 경로 | 테스트 항목 |
|
||||
|--------|------|------------|
|
||||
| 거래처 관리 | `/accounting/vendor-management` | 전화번호, 사업자번호 |
|
||||
| 직원 관리 | `/hr/employee-management` | 전화번호, 주민번호 |
|
||||
| 견적 등록 | `/quotes` | 수량, 금액 |
|
||||
| 주문 관리 | `/sales/order-management-sales` | 수량, 금액 |
|
||||
| 재고 관리 | `/material/stock-status` | 수량 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 완료 체크리스트
|
||||
|
||||
### Phase 1: 유틸리티 및 기본 컴포넌트
|
||||
- [x] formatters.ts 유틸리티 함수 생성
|
||||
- [x] PhoneInput 컴포넌트 생성
|
||||
- [x] BusinessNumberInput 컴포넌트 생성
|
||||
- [x] PersonalNumberInput 컴포넌트 생성
|
||||
- [x] NumberInput 컴포넌트 생성
|
||||
- [x] CurrencyInput 컴포넌트 생성
|
||||
- [x] QuantityInput 컴포넌트 생성
|
||||
|
||||
### Phase 2: 통합
|
||||
- [x] ui/index.ts export 추가 (개별 import 방식 사용)
|
||||
- [x] FormField 타입 확장
|
||||
|
||||
### Phase 3: 테스트 및 적용
|
||||
- [ ] 개별 컴포넌트 동작 테스트
|
||||
- [x] VendorDetail 적용 완료
|
||||
- [x] PhoneInput: phone, mobile, fax, managerPhone
|
||||
- [x] BusinessNumberInput: businessNumber (유효성 검사 포함)
|
||||
- [x] CurrencyInput: outstandingAmount, unpaidAmount
|
||||
- [x] NumberInput: overdueDays
|
||||
- [ ] 문서 최종 업데이트
|
||||
|
||||
---
|
||||
|
||||
## 7. 롤백 계획
|
||||
|
||||
문제 발생 시:
|
||||
1. 새 컴포넌트 import 제거
|
||||
2. 기존 `<Input type="number">` 컴포넌트로 복원
|
||||
3. FormField 타입 변경 롤백
|
||||
|
||||
---
|
||||
|
||||
## 8. 참고사항
|
||||
|
||||
### 기존 컴포넌트 위치
|
||||
- Input: `src/components/ui/input.tsx`
|
||||
- FormField: `src/components/molecules/FormField.tsx`
|
||||
|
||||
### 생성된 파일
|
||||
| 파일 | 경로 |
|
||||
|------|------|
|
||||
| formatters | `src/lib/formatters.ts` |
|
||||
| PhoneInput | `src/components/ui/phone-input.tsx` |
|
||||
| BusinessNumberInput | `src/components/ui/business-number-input.tsx` |
|
||||
| PersonalNumberInput | `src/components/ui/personal-number-input.tsx` |
|
||||
| NumberInput | `src/components/ui/number-input.tsx` |
|
||||
| CurrencyInput | `src/components/ui/currency-input.tsx` |
|
||||
| QuantityInput | `src/components/ui/quantity-input.tsx` |
|
||||
|
||||
---
|
||||
|
||||
## 9. 사용 예시
|
||||
|
||||
### 직접 import 방식
|
||||
```tsx
|
||||
import { PhoneInput } from '@/components/ui/phone-input';
|
||||
import { CurrencyInput } from '@/components/ui/currency-input';
|
||||
import { NumberInput } from '@/components/ui/number-input';
|
||||
|
||||
// 전화번호
|
||||
<PhoneInput value={phone} onChange={setPhone} />
|
||||
|
||||
// 금액
|
||||
<CurrencyInput value={price} onChange={setPrice} />
|
||||
|
||||
// 소수점 허용 숫자
|
||||
<NumberInput value={rate} onChange={setRate} allowDecimal decimalPlaces={2} />
|
||||
```
|
||||
|
||||
### FormField 통합 방식
|
||||
```tsx
|
||||
import { FormField } from '@/components/molecules/FormField';
|
||||
|
||||
// 전화번호
|
||||
<FormField
|
||||
label="전화번호"
|
||||
type="phone"
|
||||
value={phone}
|
||||
onChange={setPhone}
|
||||
/>
|
||||
|
||||
// 사업자번호 (유효성 검사 표시)
|
||||
<FormField
|
||||
label="사업자번호"
|
||||
type="businessNumber"
|
||||
value={bizNo}
|
||||
onChange={setBizNo}
|
||||
showValidation
|
||||
/>
|
||||
|
||||
// 금액
|
||||
<FormField
|
||||
label="금액"
|
||||
type="currency"
|
||||
value={price}
|
||||
onChangeNumber={setPrice}
|
||||
/>
|
||||
|
||||
// 수량 (+/- 버튼)
|
||||
<FormField
|
||||
label="수량"
|
||||
type="quantity"
|
||||
value={qty}
|
||||
onChangeNumber={setQty}
|
||||
showButtons
|
||||
min={1}
|
||||
max={100}
|
||||
/>
|
||||
```
|
||||
@@ -0,0 +1,372 @@
|
||||
# Phase 4: 입력 컴포넌트 전체 적용 계획서
|
||||
|
||||
**작성일**: 2026-01-21
|
||||
**작성자**: Claude Code
|
||||
**상태**: 🔵 계획 수립 완료
|
||||
**근거 문서**: [IMPL-2026-01-21] input-form-componentization.md
|
||||
|
||||
---
|
||||
|
||||
## 1. 스캔 결과 요약
|
||||
|
||||
### 1.1 대상 파일 통계
|
||||
| 카테고리 | 파일 수 | 비고 |
|
||||
|----------|--------|------|
|
||||
| `type="number"` 사용 | 52개 | 직접 Input 사용 |
|
||||
| 전화번호 관련 | 70개 | phone, tel, 전화, 연락처 |
|
||||
| 사업자번호 관련 | 33개 | businessNumber, 사업자번호 |
|
||||
| 금액 관련 | 197개 | price, amount, 금액, 단가 |
|
||||
| 수량 관련 | 106개 | quantity, qty, 수량 |
|
||||
|
||||
### 1.2 마이그레이션 접근 전략
|
||||
|
||||
**전략 1: 템플릿 레벨 수정 (최고 효율)**
|
||||
- `IntegratedDetailTemplate/FieldInput.tsx` 수정
|
||||
- `IntegratedDetailTemplate/FieldRenderer.tsx` 수정
|
||||
- 이 템플릿을 사용하는 **모든 페이지**에 자동 적용
|
||||
|
||||
**전략 2: FormField 타입 확장 (이미 완료)**
|
||||
- `FormField.tsx`에 새 타입 추가 완료
|
||||
- FormField를 사용하는 컴포넌트는 타입만 변경하면 됨
|
||||
|
||||
**전략 3: 개별 컴포넌트 수정**
|
||||
- 직접 `<Input type="number">` 사용하는 컴포넌트
|
||||
- 커스텀 로직이 있어 템플릿 적용 불가한 컴포넌트
|
||||
|
||||
---
|
||||
|
||||
## 2. 마이그레이션 우선순위
|
||||
|
||||
### 🔴 Tier 1: 템플릿 레벨 (최우선)
|
||||
> 한 번 수정으로 다수 페이지에 적용
|
||||
|
||||
| 파일 | 수정 내용 | 영향 범위 |
|
||||
|------|----------|----------|
|
||||
| `IntegratedDetailTemplate/FieldInput.tsx` | number 타입에 NumberInput/CurrencyInput 적용, phone/businessNumber 타입 추가 | 템플릿 사용 전체 |
|
||||
| `IntegratedDetailTemplate/FieldRenderer.tsx` | 동일 | 템플릿 사용 전체 |
|
||||
| `IntegratedDetailTemplate/types.ts` | FieldType에 새 타입 추가 | 타입 시스템 |
|
||||
|
||||
### 🟠 Tier 2: 핵심 폼 컴포넌트
|
||||
> 사용 빈도가 높거나 중요한 폼
|
||||
|
||||
**회계 도메인 (accounting/)**
|
||||
| 파일 | 적용 대상 | 우선순위 |
|
||||
|------|----------|----------|
|
||||
| ✅ `VendorDetail.tsx` | phone, businessNumber, currency | 완료 |
|
||||
| `PurchaseDetail.tsx` | currency (금액) | 높음 |
|
||||
| `SalesDetail.tsx` | currency (금액) | 높음 |
|
||||
| `BillDetail.tsx` | currency (금액) | 높음 |
|
||||
| `DepositDetail.tsx` | currency (금액) | 높음 |
|
||||
| `WithdrawalDetail.tsx` | currency (금액) | 높음 |
|
||||
| `BadDebtDetail.tsx` | currency, phone | 높음 |
|
||||
|
||||
**주문/견적 도메인 (orders/, quotes/)**
|
||||
| 파일 | 적용 대상 | 우선순위 |
|
||||
|------|----------|----------|
|
||||
| `OrderRegistration.tsx` | currency, quantity | 높음 |
|
||||
| `OrderSalesDetailEdit.tsx` | currency, quantity | 높음 |
|
||||
| `QuoteRegistration.tsx` | currency, quantity, number | 높음 |
|
||||
| `QuoteRegistrationV2.tsx` | currency, quantity, number | 높음 |
|
||||
| `LocationDetailPanel.tsx` | currency, quantity | 중간 |
|
||||
| `LocationListPanel.tsx` | currency, quantity | 중간 |
|
||||
|
||||
**인사 도메인 (hr/)**
|
||||
| 파일 | 적용 대상 | 우선순위 |
|
||||
|------|----------|----------|
|
||||
| `EmployeeForm.tsx` | phone, personalNumber | 높음 |
|
||||
| `EmployeeDetail.tsx` | phone, personalNumber | 높음 |
|
||||
| `EmployeeDialog.tsx` | phone | 높음 |
|
||||
| `SalaryDetailDialog.tsx` | currency | 중간 |
|
||||
| `VacationRegisterDialog.tsx` | number | 중간 |
|
||||
| `VacationGrantDialog.tsx` | number | 중간 |
|
||||
|
||||
**고객 도메인 (clients/)**
|
||||
| 파일 | 적용 대상 | 우선순위 |
|
||||
|------|----------|----------|
|
||||
| `ClientDetail.tsx` | phone, businessNumber | 높음 |
|
||||
| `ClientRegistration.tsx` | phone, businessNumber | 높음 |
|
||||
| `ClientDetailClientV2.tsx` | phone, businessNumber | 높음 |
|
||||
|
||||
### 🟡 Tier 3: 보조 컴포넌트
|
||||
> 중요하지만 사용 빈도 낮음
|
||||
|
||||
**품목 관리 (items/)**
|
||||
| 파일 | 적용 대상 |
|
||||
|------|----------|
|
||||
| `ItemDetailEdit.tsx` | currency, quantity |
|
||||
| `ItemDetailView.tsx` | currency, quantity |
|
||||
| `DynamicItemForm/` | number, currency |
|
||||
| `BOMSection.tsx` | quantity |
|
||||
| `ItemAddDialog.tsx` (orders) | quantity, currency |
|
||||
|
||||
**자재/생산 (material/, production/)**
|
||||
| 파일 | 적용 대상 |
|
||||
|------|----------|
|
||||
| `ReceivingDetail.tsx` | quantity |
|
||||
| `ReceivingProcessDialog.tsx` | quantity |
|
||||
| `StockStatusDetail.tsx` | quantity |
|
||||
| `WorkOrderDetail.tsx` | quantity |
|
||||
| `InspectionDetail.tsx` | quantity |
|
||||
| `InspectionCreate.tsx` | quantity |
|
||||
|
||||
**건설 도메인 (construction/)**
|
||||
| 파일 | 적용 대상 |
|
||||
|------|----------|
|
||||
| `ContractDetailForm.tsx` | currency |
|
||||
| `EstimateDetailForm.tsx` | currency, quantity |
|
||||
| `BiddingDetailForm.tsx` | currency |
|
||||
| `PartnerForm.tsx` | phone, businessNumber |
|
||||
| `HandoverReportDetailForm.tsx` | number |
|
||||
| `PricingDetailClient.tsx` | currency |
|
||||
| `ProgressBillingItemTable.tsx` | currency, quantity |
|
||||
| `OrderDetailItemTable.tsx` | currency, quantity |
|
||||
|
||||
### 🟢 Tier 4: 기타 컴포넌트
|
||||
> 낮은 우선순위, 점진적 적용
|
||||
|
||||
**설정 (settings/)**
|
||||
- `CompanyInfoManagement/` - businessNumber
|
||||
- `PopupManagement/` - phone
|
||||
- `AddCompanyDialog.tsx` - businessNumber
|
||||
|
||||
**결재 (approval/)**
|
||||
- `ExpenseReportForm.tsx` - currency
|
||||
- `ProposalForm.tsx` - currency
|
||||
|
||||
**문서 컴포넌트 (documents/)**
|
||||
> 대부분 표시용으로 입력 필드 없음 - 확인 필요
|
||||
- `OrderDocumentModal.tsx`
|
||||
- `TransactionDocument.tsx`
|
||||
- `ContractDocument.tsx`
|
||||
|
||||
---
|
||||
|
||||
## 3. 작업 단계별 계획
|
||||
|
||||
### Phase 4-1: 템플릿 레벨 수정 (핵심)
|
||||
**목표**: IntegratedDetailTemplate에 새 입력 타입 지원 추가
|
||||
|
||||
```
|
||||
수정 파일:
|
||||
1. src/components/templates/IntegratedDetailTemplate/types.ts
|
||||
- FieldType에 'phone' | 'businessNumber' | 'currency' | 'quantity' 추가
|
||||
|
||||
2. src/components/templates/IntegratedDetailTemplate/FieldInput.tsx
|
||||
- PhoneInput, BusinessNumberInput, CurrencyInput, QuantityInput import
|
||||
- switch case에 새 타입 처리 추가
|
||||
|
||||
3. src/components/templates/IntegratedDetailTemplate/FieldRenderer.tsx
|
||||
- 동일하게 수정
|
||||
```
|
||||
|
||||
**예상 영향**: 템플릿 사용 페이지 전체 자동 적용
|
||||
|
||||
### Phase 4-2: 회계 도메인 마이그레이션
|
||||
```
|
||||
1. PurchaseDetail.tsx → CurrencyInput
|
||||
2. SalesDetail.tsx → CurrencyInput
|
||||
3. BillDetail.tsx → CurrencyInput
|
||||
4. DepositDetail.tsx → CurrencyInput
|
||||
5. WithdrawalDetail.tsx → CurrencyInput
|
||||
6. BadDebtDetail.tsx → CurrencyInput, PhoneInput
|
||||
```
|
||||
|
||||
### Phase 4-3: 주문/견적 도메인 마이그레이션
|
||||
```
|
||||
1. OrderRegistration.tsx → CurrencyInput, QuantityInput
|
||||
2. OrderSalesDetailEdit.tsx → CurrencyInput, QuantityInput
|
||||
3. QuoteRegistration.tsx → CurrencyInput, QuantityInput, NumberInput
|
||||
4. QuoteRegistrationV2.tsx → CurrencyInput, QuantityInput, NumberInput
|
||||
```
|
||||
|
||||
### Phase 4-4: 인사 도메인 마이그레이션
|
||||
```
|
||||
1. EmployeeForm.tsx → PhoneInput, PersonalNumberInput
|
||||
2. EmployeeDetail.tsx → PhoneInput, PersonalNumberInput
|
||||
3. EmployeeDialog.tsx → PhoneInput
|
||||
4. SalaryDetailDialog.tsx → CurrencyInput
|
||||
```
|
||||
|
||||
### Phase 4-5: 고객/품목/자재 도메인 마이그레이션
|
||||
```
|
||||
1. ClientDetail.tsx → PhoneInput, BusinessNumberInput
|
||||
2. ClientRegistration.tsx → PhoneInput, BusinessNumberInput
|
||||
3. ItemDetailEdit.tsx → CurrencyInput, QuantityInput
|
||||
4. ReceivingDetail.tsx → QuantityInput
|
||||
5. StockStatusDetail.tsx → QuantityInput
|
||||
```
|
||||
|
||||
### Phase 4-6: 건설/기타 도메인 마이그레이션
|
||||
```
|
||||
1. ContractDetailForm.tsx → CurrencyInput
|
||||
2. EstimateDetailForm.tsx → CurrencyInput, QuantityInput
|
||||
3. PartnerForm.tsx → PhoneInput, BusinessNumberInput
|
||||
4. 기타 낮은 우선순위 파일들
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 마이그레이션 패턴
|
||||
|
||||
### 4.1 직접 Input → 새 컴포넌트 변환
|
||||
|
||||
**Before (기존)**:
|
||||
```tsx
|
||||
<Input
|
||||
type="number"
|
||||
value={formData.price}
|
||||
onChange={(e) => handleChange('price', e.target.value)}
|
||||
/>
|
||||
```
|
||||
|
||||
**After (CurrencyInput)**:
|
||||
```tsx
|
||||
import { CurrencyInput } from '@/components/ui/currency-input';
|
||||
|
||||
<CurrencyInput
|
||||
value={formData.price}
|
||||
onChange={(value) => handleChange('price', value ?? 0)}
|
||||
/>
|
||||
```
|
||||
|
||||
**After (PhoneInput)**:
|
||||
```tsx
|
||||
import { PhoneInput } from '@/components/ui/phone-input';
|
||||
|
||||
<PhoneInput
|
||||
value={formData.phone}
|
||||
onChange={(value) => handleChange('phone', value)}
|
||||
/>
|
||||
```
|
||||
|
||||
### 4.2 FormField 타입 변경
|
||||
|
||||
**Before**:
|
||||
```tsx
|
||||
<FormField
|
||||
label="금액"
|
||||
type="number"
|
||||
value={price}
|
||||
onChange={setPrice}
|
||||
/>
|
||||
```
|
||||
|
||||
**After**:
|
||||
```tsx
|
||||
<FormField
|
||||
label="금액"
|
||||
type="currency"
|
||||
value={price}
|
||||
onChangeNumber={setPrice}
|
||||
/>
|
||||
```
|
||||
|
||||
### 4.3 Config 기반 (IntegratedDetailTemplate)
|
||||
|
||||
**Before (config)**:
|
||||
```tsx
|
||||
{
|
||||
key: 'price',
|
||||
label: '금액',
|
||||
type: 'number',
|
||||
}
|
||||
```
|
||||
|
||||
**After (config)**:
|
||||
```tsx
|
||||
{
|
||||
key: 'price',
|
||||
label: '금액',
|
||||
type: 'currency',
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 검증 계획
|
||||
|
||||
### 5.1 각 Phase 완료 후 검증
|
||||
- [ ] TypeScript 컴파일 오류 없음
|
||||
- [ ] 해당 페이지 렌더링 정상
|
||||
- [ ] 입력 필드 동작 확인
|
||||
- 포맷팅 정상 (콤마, 하이픈 등)
|
||||
- leading zero 제거 확인
|
||||
- 값 저장/불러오기 정상
|
||||
|
||||
### 5.2 주요 테스트 시나리오
|
||||
|
||||
| 컴포넌트 | 테스트 입력 | 기대 결과 |
|
||||
|----------|------------|----------|
|
||||
| CurrencyInput | `1234567` | 표시: `₩ 1,234,567`, 값: `1234567` |
|
||||
| PhoneInput | `01012345678` | 표시: `010-1234-5678`, 값: `01012345678` |
|
||||
| BusinessNumberInput | `1234567890` | 표시: `123-45-67890`, 값: `1234567890` |
|
||||
| QuantityInput | `007` | 표시: `7`, 값: `7` |
|
||||
| NumberInput | `00123.45` | 표시: `123.45`, 값: `123.45` |
|
||||
|
||||
---
|
||||
|
||||
## 6. 롤백 계획
|
||||
|
||||
문제 발생 시:
|
||||
1. 해당 파일의 import 변경 롤백
|
||||
2. 컴포넌트 사용 부분을 기존 `<Input>` 으로 복원
|
||||
3. 템플릿 수정의 경우 FieldInput.tsx, FieldRenderer.tsx 롤백
|
||||
|
||||
---
|
||||
|
||||
## 7. 진행 상황 체크리스트
|
||||
|
||||
### Phase 4-1: 템플릿 수정
|
||||
- [ ] IntegratedDetailTemplate/types.ts 수정
|
||||
- [ ] IntegratedDetailTemplate/FieldInput.tsx 수정
|
||||
- [ ] IntegratedDetailTemplate/FieldRenderer.tsx 수정
|
||||
- [ ] 템플릿 사용 페이지 동작 확인
|
||||
|
||||
### Phase 4-2: 회계 도메인
|
||||
- [ ] PurchaseDetail.tsx
|
||||
- [ ] SalesDetail.tsx
|
||||
- [ ] BillDetail.tsx
|
||||
- [ ] DepositDetail.tsx
|
||||
- [ ] WithdrawalDetail.tsx
|
||||
- [ ] BadDebtDetail.tsx
|
||||
|
||||
### Phase 4-3: 주문/견적 도메인
|
||||
- [ ] OrderRegistration.tsx
|
||||
- [ ] OrderSalesDetailEdit.tsx
|
||||
- [ ] QuoteRegistration.tsx
|
||||
- [ ] QuoteRegistrationV2.tsx
|
||||
|
||||
### Phase 4-4: 인사 도메인
|
||||
- [ ] EmployeeForm.tsx
|
||||
- [ ] EmployeeDetail.tsx
|
||||
- [ ] EmployeeDialog.tsx
|
||||
- [ ] SalaryDetailDialog.tsx
|
||||
|
||||
### Phase 4-5: 고객/품목/자재 도메인
|
||||
- [ ] ClientDetail.tsx
|
||||
- [ ] ClientRegistration.tsx
|
||||
- [ ] ItemDetailEdit.tsx
|
||||
- [ ] ReceivingDetail.tsx
|
||||
- [ ] StockStatusDetail.tsx
|
||||
|
||||
### Phase 4-6: 건설/기타 도메인
|
||||
- [ ] ContractDetailForm.tsx
|
||||
- [ ] EstimateDetailForm.tsx
|
||||
- [ ] PartnerForm.tsx
|
||||
- [ ] 기타 파일들
|
||||
|
||||
---
|
||||
|
||||
## 8. 다음 단계
|
||||
|
||||
1. **즉시**: Phase 4-1 템플릿 레벨 수정 (최대 효과)
|
||||
2. **순차**: Phase 4-2 ~ 4-6 도메인별 마이그레이션
|
||||
3. **최종**: 전체 빌드 및 통합 테스트
|
||||
|
||||
---
|
||||
|
||||
**참고**: VendorDetail.tsx 적용 결과 검증 완료됨 (2026-01-21)
|
||||
- PhoneInput ✅
|
||||
- BusinessNumberInput ✅
|
||||
- CurrencyInput ✅
|
||||
- NumberInput ✅
|
||||
@@ -0,0 +1,304 @@
|
||||
# 상세 페이지 훅 마이그레이션 계획서
|
||||
|
||||
> 작성일: 2026-02-05
|
||||
> 상태: 계획 수립
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요
|
||||
|
||||
### 1.1 목적
|
||||
- 상세/등록/수정 페이지의 반복 코드를 공통 훅으로 통합
|
||||
- 코드 일관성 확보 및 유지보수성 향상
|
||||
- 서비스 런칭 전 기술 부채 최소화
|
||||
|
||||
### 1.2 생성된 공통 훅
|
||||
| 훅 | 위치 | 역할 |
|
||||
|----|------|------|
|
||||
| `useDetailPageState` | `src/hooks/useDetailPageState.ts` | 페이지 상태 관리 (mode, id, navigation) |
|
||||
| `useDetailData` | `src/hooks/useDetailData.ts` | 데이터 로딩 + 로딩/에러 상태 |
|
||||
| `useCRUDHandlers` | `src/hooks/useCRUDHandlers.ts` | 등록/수정/삭제 + toast/redirect |
|
||||
| `useDetailPermissions` | `src/hooks/useDetailPermissions.ts` | 권한 체크 |
|
||||
|
||||
### 1.3 테스트 완료
|
||||
- [x] `BillDetail.tsx` → `BillDetailV2.tsx` 마이그레이션 성공
|
||||
- [x] 조회/수정/등록 모드 정상 작동 확인
|
||||
- [x] 유효성 검사 정상 작동 확인
|
||||
|
||||
---
|
||||
|
||||
## 2. 마이그레이션 대상
|
||||
|
||||
### 2.1 전체 현황
|
||||
| 구분 | 개수 | 비고 |
|
||||
|------|------|------|
|
||||
| IntegratedDetailTemplate 사용 | 47개 | 훅 마이그레이션 대상 |
|
||||
| 레거시/커스텀 패턴 | 36개 | 별도 검토 (이번 범위 외) |
|
||||
| **총계** | 83개 | |
|
||||
|
||||
### 2.2 복잡도별 분류
|
||||
| 복잡도 | 기준 | 개수 |
|
||||
|--------|------|------|
|
||||
| 단순 | < 200줄, useState 3~4개 | 12개 |
|
||||
| 보통 | 200~500줄, useState 5~7개 | 18개 |
|
||||
| 복잡 | > 500줄, useState 8~11개 | 17개 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 도메인별 대상 목록
|
||||
|
||||
### 3.1 회계관리 (10개)
|
||||
|
||||
| # | 파일 | 라인 | 복잡도 | 상태 |
|
||||
|---|------|------|--------|------|
|
||||
| 1 | `accounting/BadDebtCollection/BadDebtDetail.tsx` | 966 | 복잡 | ⬜ |
|
||||
| 2 | `accounting/BillManagement/BillDetail.tsx` | 474 | 보통 | ✅ 완료 |
|
||||
| 3 | `accounting/CardTransactionInquiry/CardTransactionDetailClient.tsx` | 138 | 단순 | ⬜ |
|
||||
| 4 | `accounting/DepositManagement/DepositDetailClientV2.tsx` | 143 | 단순 | ⬜ |
|
||||
| 5 | `accounting/PurchaseManagement/PurchaseDetail.tsx` | 698 | 복잡 | ⬜ |
|
||||
| 6 | `accounting/SalesManagement/SalesDetail.tsx` | 581 | 복잡 | ⬜ |
|
||||
| 7 | `accounting/VendorLedger/VendorLedgerDetail.tsx` | 385 | 보통 | ⬜ |
|
||||
| 8 | `accounting/VendorManagement/VendorDetail.tsx` | 683 | 복잡 | ⬜ |
|
||||
| 9 | `accounting/VendorManagement/VendorDetailClient.tsx` | 585 | 복잡 | ⬜ |
|
||||
| 10 | `accounting/WithdrawalManagement/WithdrawalDetail.tsx` | 327 | 보통 | ⬜ |
|
||||
|
||||
### 3.2 건설관리 (13개)
|
||||
|
||||
| # | 파일 | 라인 | 복잡도 | 상태 |
|
||||
|---|------|------|--------|------|
|
||||
| 1 | `construction/bidding/BiddingDetailForm.tsx` | 544 | 복잡 | ⬜ |
|
||||
| 2 | `construction/contract/ContractDetailForm.tsx` | 546 | 복잡 | ⬜ |
|
||||
| 3 | `construction/estimates/EstimateDetailForm.tsx` | 763 | 복잡 | ⬜ |
|
||||
| 4 | `construction/handover-report/HandoverReportDetailForm.tsx` | 699 | 복잡 | ⬜ |
|
||||
| 5 | `construction/issue-management/IssueDetailForm.tsx` | 627 | 복잡 | ⬜ |
|
||||
| 6 | `construction/item-management/ItemDetailClient.tsx` | 486 | 보통 | ⬜ |
|
||||
| 7 | `construction/labor-management/LaborDetailClientV2.tsx` | 120 | 단순 | ⬜ |
|
||||
| 8 | `construction/management/ConstructionDetailClient.tsx` | 739 | 복잡 | ⬜ |
|
||||
| 9 | `construction/order-management/OrderDetailForm.tsx` | 275 | 보통 | ⬜ |
|
||||
| 10 | `construction/pricing-management/PricingDetailClientV2.tsx` | 134 | 단순 | ⬜ |
|
||||
| 11 | `construction/progress-billing/ProgressBillingDetailForm.tsx` | 193 | 단순 | ⬜ |
|
||||
| 12 | `construction/site-management/SiteDetailForm.tsx` | 385 | 보통 | ⬜ |
|
||||
| 13 | `construction/structure-review/StructureReviewDetailForm.tsx` | 392 | 보통 | ⬜ |
|
||||
|
||||
### 3.3 기타 도메인 (24개)
|
||||
|
||||
#### 고객센터 (3개)
|
||||
| # | 파일 | 라인 | 복잡도 | 상태 |
|
||||
|---|------|------|--------|------|
|
||||
| 1 | `customer-center/EventManagement/EventDetail.tsx` | 101 | 단순 | ⬜ |
|
||||
| 2 | `customer-center/InquiryManagement/InquiryDetail.tsx` | 357 | 보통 | ⬜ |
|
||||
| 3 | `customer-center/NoticeManagement/NoticeDetail.tsx` | 101 | 단순 | ⬜ |
|
||||
|
||||
#### 인사관리 (1개)
|
||||
| # | 파일 | 라인 | 복잡도 | 상태 |
|
||||
|---|------|------|--------|------|
|
||||
| 1 | `hr/EmployeeManagement/EmployeeDetail.tsx` | 221 | 단순 | ⬜ |
|
||||
|
||||
#### 자재관리 (2개)
|
||||
| # | 파일 | 라인 | 복잡도 | 상태 |
|
||||
|---|------|------|--------|------|
|
||||
| 1 | `material/ReceivingManagement/ReceivingDetail.tsx` | ~350 | 보통 | ⬜ |
|
||||
| 2 | `material/StockStatus/StockStatusDetail.tsx` | ~300 | 보통 | ⬜ |
|
||||
|
||||
#### 주문관리 (2개)
|
||||
| # | 파일 | 라인 | 복잡도 | 상태 |
|
||||
|---|------|------|--------|------|
|
||||
| 1 | `orders/OrderSalesDetailEdit.tsx` | 735 | 복잡 | ⬜ |
|
||||
| 2 | `orders/OrderSalesDetailView.tsx` | 668 | 복잡 | ⬜ |
|
||||
|
||||
#### 출고관리 (2개)
|
||||
| # | 파일 | 라인 | 복잡도 | 상태 |
|
||||
|---|------|------|--------|------|
|
||||
| 1 | `outbound/ShipmentManagement/ShipmentDetail.tsx` | 670 | 복잡 | ⬜ |
|
||||
| 2 | `outbound/VehicleDispatchManagement/VehicleDispatchDetail.tsx` | 180 | 단순 | ⬜ |
|
||||
|
||||
#### 생산관리 (1개)
|
||||
| # | 파일 | 라인 | 복잡도 | 상태 |
|
||||
|---|------|------|--------|------|
|
||||
| 1 | `production/WorkOrders/WorkOrderDetail.tsx` | 531 | 복잡 | ⬜ |
|
||||
|
||||
#### 품질관리 (1개)
|
||||
| # | 파일 | 라인 | 복잡도 | 상태 |
|
||||
|---|------|------|--------|------|
|
||||
| 1 | `quality/InspectionManagement/InspectionDetail.tsx` | 949 | 복잡 | ⬜ |
|
||||
|
||||
#### 설정 (2개)
|
||||
| # | 파일 | 라인 | 복잡도 | 상태 |
|
||||
|---|------|------|--------|------|
|
||||
| 1 | `settings/PermissionManagement/PermissionDetail.tsx` | 455 | 보통 | ⬜ |
|
||||
| 2 | `settings/PopupManagement/PopupDetailClientV2.tsx` | 198 | 단순 | ⬜ |
|
||||
|
||||
#### 거래처 (1개)
|
||||
| # | 파일 | 라인 | 복잡도 | 상태 |
|
||||
|---|------|------|--------|------|
|
||||
| 1 | `clients/ClientDetailClientV2.tsx` | 252 | 단순 | ⬜ |
|
||||
|
||||
#### 기타 (9개)
|
||||
| # | 파일 | 라인 | 복잡도 | 상태 |
|
||||
|---|------|------|--------|------|
|
||||
| 1 | `board/BoardManagement/BoardDetail.tsx` | 119 | 단순 | ⬜ |
|
||||
| 2 | `process-management/ProcessDetail.tsx` | 346 | 보통 | ⬜ |
|
||||
| 3 | `process-management/StepDetail.tsx` | 143 | 단순 | ⬜ |
|
||||
| 4 | `settings/AccountManagement/AccountDetail.tsx` | 355 | 보통 | ⬜ |
|
||||
| 5 | `accounting/DepositManagement/DepositDetail.tsx` | 327 | 보통 | ⬜ |
|
||||
| 6 | `clients/ClientDetail.tsx` | 253 | 보통 | ⬜ |
|
||||
| 7 | `construction/labor-management/LaborDetailClient.tsx` | 471 | 보통 | ⬜ |
|
||||
| 8 | `construction/pricing-management/PricingDetailClient.tsx` | 464 | 보통 | ⬜ |
|
||||
| 9 | `quotes/LocationDetailPanel.tsx` | 826 | 복잡 | ⬜ |
|
||||
|
||||
---
|
||||
|
||||
## 4. 작업 방식
|
||||
|
||||
### 4.1 Git 브랜치 전략
|
||||
```
|
||||
main
|
||||
└── feature/detail-hooks-migration
|
||||
├── 회계관리 커밋
|
||||
├── 건설관리 커밋
|
||||
└── 기타 도메인 커밋
|
||||
```
|
||||
|
||||
### 4.2 파일별 작업 순서
|
||||
1. 파일 읽기 및 현재 패턴 파악
|
||||
2. `useDetailData` 적용 (데이터 로딩 부분)
|
||||
3. `useCRUDHandlers` 적용 (CRUD 핸들러 부분)
|
||||
4. 개별 useState → 통합 formData 객체로 변환 (선택)
|
||||
5. 기능 테스트
|
||||
6. 커밋
|
||||
|
||||
### 4.3 적용할 변경 패턴
|
||||
|
||||
#### Before (기존)
|
||||
```tsx
|
||||
const [data, setData] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(id).then(result => {
|
||||
if (result.success) setData(result.data);
|
||||
else setError(result.error);
|
||||
}).finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const result = await updateData(id, formData);
|
||||
if (result.success) {
|
||||
toast.success('저장되었습니다.');
|
||||
router.push('/list');
|
||||
} else {
|
||||
toast.error(result.error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### After (신규)
|
||||
```tsx
|
||||
const { data, isLoading, error } = useDetailData(id, fetchData);
|
||||
|
||||
const { handleUpdate, isSubmitting } = useCRUDHandlers({
|
||||
onUpdate: updateData,
|
||||
successRedirect: '/list',
|
||||
successMessages: { update: '저장되었습니다.' },
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 일정 계획
|
||||
|
||||
| Phase | 대상 | 파일 수 | 예상 기간 |
|
||||
|-------|------|---------|----------|
|
||||
| Phase 1 | 회계관리 | 10개 | 1일 |
|
||||
| Phase 2 | 건설관리 | 13개 | 1.5일 |
|
||||
| Phase 3 | 기타 도메인 | 24개 | 2일 |
|
||||
| Phase 4 | 통합 테스트 | - | 1일 |
|
||||
| **총계** | | **47개** | **약 5~6일** |
|
||||
|
||||
---
|
||||
|
||||
## 6. 체크리스트
|
||||
|
||||
### 6.1 사전 준비
|
||||
- [x] 공통 훅 4개 생성 완료
|
||||
- [x] 테스트 마이그레이션 (BillDetail) 완료
|
||||
- [x] 계획서 작성
|
||||
- [ ] 브랜치 생성
|
||||
|
||||
### 6.2 Phase 1: 회계관리 (0/10)
|
||||
- [ ] BadDebtDetail.tsx
|
||||
- [x] BillDetail.tsx ✅
|
||||
- [ ] CardTransactionDetailClient.tsx
|
||||
- [ ] DepositDetailClientV2.tsx
|
||||
- [ ] PurchaseDetail.tsx
|
||||
- [ ] SalesDetail.tsx
|
||||
- [ ] VendorLedgerDetail.tsx
|
||||
- [ ] VendorDetail.tsx
|
||||
- [ ] VendorDetailClient.tsx
|
||||
- [ ] WithdrawalDetail.tsx
|
||||
|
||||
### 6.3 Phase 2: 건설관리 (0/13)
|
||||
- [ ] BiddingDetailForm.tsx
|
||||
- [ ] ContractDetailForm.tsx
|
||||
- [ ] EstimateDetailForm.tsx
|
||||
- [ ] HandoverReportDetailForm.tsx
|
||||
- [ ] IssueDetailForm.tsx
|
||||
- [ ] ItemDetailClient.tsx
|
||||
- [ ] LaborDetailClientV2.tsx
|
||||
- [ ] ConstructionDetailClient.tsx
|
||||
- [ ] OrderDetailForm.tsx
|
||||
- [ ] PricingDetailClientV2.tsx
|
||||
- [ ] ProgressBillingDetailForm.tsx
|
||||
- [ ] SiteDetailForm.tsx
|
||||
- [ ] StructureReviewDetailForm.tsx
|
||||
|
||||
### 6.4 Phase 3: 기타 도메인 (0/24)
|
||||
- [ ] EventDetail.tsx
|
||||
- [ ] InquiryDetail.tsx
|
||||
- [ ] NoticeDetail.tsx
|
||||
- [ ] EmployeeDetail.tsx
|
||||
- [ ] ReceivingDetail.tsx
|
||||
- [ ] StockStatusDetail.tsx
|
||||
- [ ] OrderSalesDetailEdit.tsx
|
||||
- [ ] OrderSalesDetailView.tsx
|
||||
- [ ] ShipmentDetail.tsx
|
||||
- [ ] VehicleDispatchDetail.tsx
|
||||
- [ ] WorkOrderDetail.tsx
|
||||
- [ ] InspectionDetail.tsx
|
||||
- [ ] PermissionDetail.tsx
|
||||
- [ ] PopupDetailClientV2.tsx
|
||||
- [ ] ClientDetailClientV2.tsx
|
||||
- [ ] BoardDetail.tsx
|
||||
- [ ] ProcessDetail.tsx
|
||||
- [ ] StepDetail.tsx
|
||||
- [ ] AccountDetail.tsx
|
||||
- [ ] DepositDetail.tsx
|
||||
- [ ] ClientDetail.tsx
|
||||
- [ ] LaborDetailClient.tsx
|
||||
- [ ] PricingDetailClient.tsx
|
||||
- [ ] LocationDetailPanel.tsx
|
||||
|
||||
### 6.5 완료 후
|
||||
- [ ] 전체 기능 테스트
|
||||
- [ ] 코드 리뷰
|
||||
- [ ] PR 머지
|
||||
- [ ] BillDetailV2.tsx 정리 (원본으로 교체)
|
||||
|
||||
---
|
||||
|
||||
## 7. 위험 요소 및 대응
|
||||
|
||||
| 위험 | 가능성 | 대응 |
|
||||
|------|--------|------|
|
||||
| 기존 기능 손상 | 중 | 파일별 테스트, Git 롤백 준비 |
|
||||
| 예상보다 복잡한 파일 | 중 | 복잡한 파일은 부분 적용 허용 |
|
||||
| 타입 에러 | 높 | 래퍼 함수로 타입 호환성 확보 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 참고 자료
|
||||
|
||||
- 공통 훅 소스: `src/hooks/index.ts`
|
||||
- 테스트 케이스: `BillDetailV2.tsx`
|
||||
- 기존 템플릿: `IntegratedDetailTemplate.tsx`
|
||||
@@ -0,0 +1,88 @@
|
||||
# 금액/날짜 포맷터 공통화 계획
|
||||
|
||||
> 작성일: 2026-02-05
|
||||
> 상태: ✅ 완료
|
||||
> 목적: 중복 정의된 formatAmount, formatDate 함수를 공통 유틸로 통합
|
||||
|
||||
---
|
||||
|
||||
## 📊 현황 분석
|
||||
|
||||
### 이미 존재하는 유틸
|
||||
|
||||
| 파일 | 함수 | 설명 |
|
||||
|------|------|------|
|
||||
| `src/utils/formatAmount.ts` | `formatAmount()` | 자동 만원 변환 (1만 이상 → "N만원") |
|
||||
| | `formatAmountWon()` | 항상 원 단위 ("N원") |
|
||||
| | `formatAmountManwon()` | 항상 만원 단위 ("N만원") |
|
||||
| | `formatKoreanAmount()` | 억/만 축약 ("1억 5,000만") |
|
||||
| | `formatNumber()` | **신규** 단순 천단위 콤마 |
|
||||
| `src/utils/date.ts` | `getLocalDateString()` | YYYY-MM-DD 반환 |
|
||||
| | `getTodayString()` | 오늘 날짜 YYYY-MM-DD |
|
||||
| | `formatDateForInput()` | input용 날짜 변환 |
|
||||
| | `formatDate()` | **신규** YYYY-MM-DD 표시용 |
|
||||
| | `formatDateRange()` | **신규** "시작 ~ 종료" 형식 |
|
||||
|
||||
---
|
||||
|
||||
## 📊 결과 요약
|
||||
|
||||
### 마이그레이션 완료 파일
|
||||
|
||||
#### formatAmount → formatNumber (12개 파일) ✅
|
||||
| 파일 | 상태 |
|
||||
|------|------|
|
||||
| `construction/contract/ContractListClient.tsx` | ✅ 완료 |
|
||||
| `construction/contract/ContractDetailForm.tsx` | ✅ 완료 |
|
||||
| `construction/bidding/BiddingListClient.tsx` | ✅ 완료 |
|
||||
| `construction/bidding/BiddingDetailForm.tsx` | ✅ 완료 |
|
||||
| `construction/estimates/EstimateListClient.tsx` | ✅ 완료 |
|
||||
| `construction/estimates/modals/EstimateDocumentContent.tsx` | ✅ 완료 |
|
||||
| `construction/handover-report/HandoverReportListClient.tsx` | ✅ 완료 |
|
||||
| `construction/handover-report/HandoverReportDetailForm.tsx` | ✅ 완료 |
|
||||
| `construction/handover-report/modals/HandoverReportDocumentModal.tsx` | ✅ 완료 |
|
||||
| `construction/utility-management/UtilityManagementListClient.tsx` | ✅ 완료 |
|
||||
| `construction/estimates/utils/formatters.ts` | ✅ re-export로 변경 |
|
||||
|
||||
#### formatDate 공통화 (7개 파일) ✅
|
||||
| 파일 | 상태 |
|
||||
|------|------|
|
||||
| `construction/contract/ContractListClient.tsx` | ✅ 완료 (formatDateRange) |
|
||||
| `construction/bidding/BiddingListClient.tsx` | ✅ 완료 |
|
||||
| `construction/handover-report/HandoverReportListClient.tsx` | ✅ 완료 (formatDateRange) |
|
||||
| `construction/utility-management/UtilityManagementListClient.tsx` | ✅ 완료 |
|
||||
| `construction/issue-management/IssueManagementListClient.tsx` | ✅ 완료 |
|
||||
| `construction/structure-review/StructureReviewListClient.tsx` | ✅ 완료 |
|
||||
| `construction/management/ConstructionDetailClient.tsx` | ✅ 완료 |
|
||||
|
||||
#### 마이그레이션 제외 (한글 형식 유지)
|
||||
| 파일 | 사유 |
|
||||
|------|------|
|
||||
| `handover-report/modals/HandoverReportDocumentModal.tsx` | 한글 형식 ("년 월 일") |
|
||||
| `order-management/modals/OrderDocumentModal.tsx` | 한글 형식 ("년 월 일") |
|
||||
|
||||
---
|
||||
|
||||
## 📋 효과
|
||||
|
||||
| 항목 | Before | After |
|
||||
|------|--------|-------|
|
||||
| formatAmount 정의 | 12개 파일 | 1개 파일 (`formatNumber`) |
|
||||
| formatDate 정의 | 8개 파일 | 1개 파일 |
|
||||
| 중복 코드 라인 | ~150줄 | 0줄 |
|
||||
| 포맷 변경 시 수정 | 20개 파일 | 1개 파일 |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 주의사항
|
||||
|
||||
1. **기존 formatAmount()와 formatNumber() 차이**
|
||||
- 기존 `formatAmount()`: 자동 만원 변환 (유지됨)
|
||||
- 신규 `formatNumber()`: 단순 천단위 콤마만
|
||||
|
||||
2. **한글 날짜 형식은 별도 유지**
|
||||
- 문서 모달에서 사용하는 "년 월 일" 형식은 로컬 유지
|
||||
- 공통 `formatDate()`는 YYYY-MM-DD 형식만 처리
|
||||
|
||||
3. **backward compatibility**
|
||||
- `estimates/utils/formatters.ts`는 `formatNumber`를 `formatAmount`로 re-export
|
||||
@@ -0,0 +1,343 @@
|
||||
# 동적 필드 타입 컴포넌트 — 프론트엔드 구현 기획서
|
||||
|
||||
> 작성일: 2026-02-11
|
||||
> 설계 근거: `[DESIGN-2026-02-11] dynamic-field-type-extension.md`
|
||||
> 상태: ✅ 프론트 구현 완료 — 백엔드 작업 대기
|
||||
> 백엔드 스펙: `item-master/[API-REQUEST-2026-02-12] dynamic-field-type-backend-spec.md`
|
||||
|
||||
---
|
||||
|
||||
## 목적
|
||||
|
||||
현재 DynamicItemForm의 필드 타입이 6종(textbox, number, dropdown, checkbox, date, textarea)으로 제한되어 있어 제조/공사/유통/물류 등 다양한 산업의 품목관리 요구를 충족하지 못함.
|
||||
|
||||
**이 작업의 목표**:
|
||||
- 8종의 신규 필드 컴포넌트를 미리 만들어둔다
|
||||
- 범용 테이블 섹션(DynamicTableSection)을 만든다
|
||||
- 백엔드가 `field_type` + `properties` config를 보내면 자동 렌더링되는 구조를 완성한다
|
||||
- 백엔드 작업 전에도 mock props로 독립 테스트 가능하게 한다
|
||||
|
||||
**API 연동 시 동작 흐름**:
|
||||
```
|
||||
백엔드 DB: field_type = "reference", properties = { "source": "vendors" }
|
||||
↓
|
||||
API 응답: GET /v1/item-master/pages/{id}/structure
|
||||
↓
|
||||
프론트: DynamicFieldRenderer → switch("reference") → <ReferenceField />
|
||||
ReferenceField가 properties.source 읽어서 /api/proxy/vendors 검색 API 호출
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 컴포넌트 구현 목록
|
||||
|
||||
### Phase 1: 핵심 컴포넌트
|
||||
|
||||
| # | 컴포넌트 | field_type | 상태 | 비고 |
|
||||
|---|---------|-----------|------|------|
|
||||
| 1-1 | ReferenceField | `reference` | ✅ 완료 | 다른 테이블 검색/선택 |
|
||||
| 1-2 | MultiSelectField | `multi-select` | ✅ 완료 | 복수 선택 태그 |
|
||||
| 1-3 | FileField | `file` | ✅ 완료 | 파일/이미지 업로드 |
|
||||
| 1-4 | DynamicTableSection | (섹션) | ✅ 완료 | 범용 테이블 섹션 |
|
||||
| 1-5 | TableCellRenderer | (내부) | ✅ 완료 | 테이블 셀 렌더러 |
|
||||
| 1-6 | reference-sources.ts | (config) | ✅ 완료 | 참조 소스 프리셋 정의 |
|
||||
| 1-7 | DynamicFieldRenderer 확장 | (수정) | ✅ 완료 | switch문 + 신규 import |
|
||||
| 1-8 | 타입 정의 확장 | (수정) | ✅ 완료 | ItemFieldType 통합 + config 인터페이스 |
|
||||
|
||||
### Phase 2: 편의 컴포넌트
|
||||
|
||||
| # | 컴포넌트 | field_type | 상태 | 비고 |
|
||||
|---|---------|-----------|------|------|
|
||||
| 2-1 | CurrencyField | `currency` | ✅ 완료 | 통화 금액 (천단위 포맷) |
|
||||
| 2-2 | UnitValueField | `unit-value` | ✅ 완료 | 값+단위 조합 (100mm) |
|
||||
| 2-3 | RadioField | `radio` | ✅ 완료 | 라디오 버튼 그룹 |
|
||||
| 2-4 | section-presets.ts | (config) | ✅ 완료 | 산업별 섹션 프리셋 JSON |
|
||||
|
||||
### Phase 3: 고급 컴포넌트
|
||||
|
||||
| # | 컴포넌트 | field_type | 상태 | 비고 |
|
||||
|---|---------|-----------|------|------|
|
||||
| 3-1 | ToggleField | `toggle` | ✅ 완료 | On/Off 스위치 |
|
||||
| 3-2 | ComputedField | `computed` | ✅ 완료 | 계산 필드 (읽기전용) |
|
||||
| 3-3 | 조건부 표시 연산자 확장 | (수정) | ✅ 완료 | in/not_in/greater_than 등 9종 |
|
||||
|
||||
---
|
||||
|
||||
## 각 컴포넌트 스펙
|
||||
|
||||
### 1-1. ReferenceField
|
||||
|
||||
**파일**: `DynamicItemForm/fields/ReferenceField.tsx`
|
||||
|
||||
**역할**: 다른 테이블의 데이터를 검색하여 선택 (거래처, 품목, 고객, 현장, 차량 등)
|
||||
|
||||
**UI 구성**:
|
||||
- 읽기전용 Input + 검색 버튼(돋보기 아이콘)
|
||||
- 클릭 시 SearchableSelectionModal 열림
|
||||
- 선택 후 displayField 값 표시
|
||||
- X 버튼으로 선택 해제
|
||||
|
||||
**properties에서 읽는 값**:
|
||||
```typescript
|
||||
interface ReferenceConfig {
|
||||
source: string; // "vendors" | "items" | "custom" 등
|
||||
displayField?: string; // 기본 "name"
|
||||
valueField?: string; // 기본 "id"
|
||||
searchFields?: string[]; // 기본 ["name"]
|
||||
searchApiUrl?: string; // source="custom"일 때 필수
|
||||
columns?: Array<{ key: string; label: string; width?: string }>;
|
||||
displayFormat?: string; // "{code} - {name}"
|
||||
returnFields?: string[]; // ["id", "code", "name"]
|
||||
}
|
||||
```
|
||||
|
||||
**API 연동 시**:
|
||||
- `REFERENCE_SOURCES[source]`에서 apiUrl 조회
|
||||
- `GET {apiUrl}?search={query}&size=20` 호출
|
||||
- 결과를 SearchableSelectionModal에 표시
|
||||
|
||||
**API 연동 전 (mock)**:
|
||||
- props로 전달된 options 사용 또는
|
||||
- 빈 상태에서 UI/UX만 확인
|
||||
|
||||
---
|
||||
|
||||
### 1-2. MultiSelectField
|
||||
|
||||
**파일**: `DynamicItemForm/fields/MultiSelectField.tsx`
|
||||
|
||||
**역할**: 여러 항목을 동시에 선택 (태그 칩 형태로 표시)
|
||||
|
||||
**UI 구성**:
|
||||
- Combobox (검색 가능한 드롭다운)
|
||||
- 선택된 항목은 칩(Chip/Badge)으로 표시
|
||||
- 칩의 X 버튼으로 개별 해제
|
||||
|
||||
**properties에서 읽는 값**:
|
||||
```typescript
|
||||
interface MultiSelectConfig {
|
||||
maxSelections?: number; // 최대 선택 수 (기본: 무제한)
|
||||
allowCustom?: boolean; // 직접 입력 허용 (기본: false)
|
||||
layout?: 'chips' | 'list'; // 기본: "chips"
|
||||
}
|
||||
```
|
||||
|
||||
**options**: 기존 dropdown과 동일 `[{label, value}]`
|
||||
|
||||
**저장값**: `string[]` (예: `["CUT", "BEND", "WELD"]`)
|
||||
|
||||
---
|
||||
|
||||
### 1-3. FileField
|
||||
|
||||
**파일**: `DynamicItemForm/fields/FileField.tsx`
|
||||
|
||||
**역할**: 파일/이미지 첨부
|
||||
|
||||
**UI 구성**:
|
||||
- 파일 선택 버튼 ("파일 선택" 또는 드래그 앤 드롭 영역)
|
||||
- 선택된 파일 목록 표시 (이름, 크기, 삭제 버튼)
|
||||
- 이미지 파일일 경우 미리보기 썸네일
|
||||
|
||||
**properties에서 읽는 값**:
|
||||
```typescript
|
||||
interface FileConfig {
|
||||
accept?: string; // ".pdf,.doc" (기본: "*")
|
||||
maxSize?: number; // bytes (기본: 10MB = 10485760)
|
||||
maxFiles?: number; // 기본: 1
|
||||
preview?: boolean; // 이미지 미리보기 (기본: true)
|
||||
category?: string; // 파일 카테고리 태그
|
||||
}
|
||||
```
|
||||
|
||||
**API 연동 시**: `POST /v1/files/upload` (multipart)
|
||||
**API 연동 전**: File 객체를 로컬 상태로 관리, URL.createObjectURL로 미리보기
|
||||
|
||||
---
|
||||
|
||||
### 1-4. DynamicTableSection
|
||||
|
||||
**파일**: `DynamicItemForm/sections/DynamicTableSection.tsx`
|
||||
|
||||
**역할**: config 기반 범용 테이블 (공정, 품질검사, 구매처, 공정표, 배차 등)
|
||||
|
||||
**UI 구성**:
|
||||
- 테이블 헤더 (columns config 기반)
|
||||
- 행 추가/삭제 버튼
|
||||
- 각 셀은 TableCellRenderer (= DynamicFieldRenderer 재사용)
|
||||
- 요약행 (선택, summaryRow config)
|
||||
- 빈 상태 메시지
|
||||
|
||||
**props**:
|
||||
```typescript
|
||||
interface DynamicTableSectionProps {
|
||||
section: ItemSectionResponse;
|
||||
tableConfig: TableConfig;
|
||||
rows: Record<string, any>[];
|
||||
onRowsChange: (rows: Record<string, any>[]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
**tableConfig 구조**: 설계서 섹션 4.3 참조
|
||||
|
||||
**API 연동 시**: `GET/PUT /v1/items/{itemId}/section-data/{sectionId}`
|
||||
**API 연동 전**: rows를 폼 상태(formData)에 로컬 관리
|
||||
|
||||
---
|
||||
|
||||
### 1-5. TableCellRenderer
|
||||
|
||||
**파일**: `DynamicItemForm/sections/TableCellRenderer.tsx`
|
||||
|
||||
**역할**: 테이블 셀 = DynamicFieldRenderer를 테이블 셀용 축소 모드로 래핑
|
||||
|
||||
**핵심**: column config → ItemFieldResponse 호환 객체로 변환 → DynamicFieldRenderer 호출
|
||||
|
||||
```typescript
|
||||
function TableCellRenderer({ column, value, onChange, compact }) {
|
||||
const fieldLike = columnToFieldResponse(column);
|
||||
return <DynamicFieldRenderer field={fieldLike} value={value} onChange={onChange} />;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1-6. reference-sources.ts
|
||||
|
||||
**파일**: `DynamicItemForm/config/reference-sources.ts`
|
||||
|
||||
**역할**: reference 필드의 소스별 기본 설정 (API URL, 표시 필드, 검색 컬럼)
|
||||
|
||||
**내용**: 공통(vendors, items, customers, employees, warehouses) + 산업별(processes, sites, vehicles, stores 등)
|
||||
|
||||
**확장 방법**: 새 소스 추가 = 이 파일에 객체 1개 추가
|
||||
|
||||
---
|
||||
|
||||
### 2-1. CurrencyField
|
||||
|
||||
**파일**: `DynamicItemForm/fields/CurrencyField.tsx`
|
||||
|
||||
**역할**: 통화 금액 입력 (천단위 콤마, 통화 기호)
|
||||
|
||||
**UI**: Input + 통화기호(₩) prefix + 천단위 포맷
|
||||
- 입력 중: 숫자만
|
||||
- 포커스 아웃: "₩15,000" 포맷
|
||||
|
||||
**properties**: `{ currency, precision, showSymbol, allowNegative }`
|
||||
|
||||
**저장값**: `number` (포맷 없이)
|
||||
|
||||
---
|
||||
|
||||
### 2-2. UnitValueField
|
||||
|
||||
**파일**: `DynamicItemForm/fields/UnitValueField.tsx`
|
||||
|
||||
**역할**: 값 + 단위 조합 입력 (100mm, 50kg)
|
||||
|
||||
**UI**: Input(숫자) + Select(단위) 가로 배치
|
||||
|
||||
**properties**: `{ units, defaultUnit, precision }`
|
||||
|
||||
**저장값**: `{ value: number, unit: string }`
|
||||
|
||||
---
|
||||
|
||||
### 2-3. RadioField
|
||||
|
||||
**파일**: `DynamicItemForm/fields/RadioField.tsx`
|
||||
|
||||
**역할**: 라디오 버튼 그룹
|
||||
|
||||
**UI**: RadioGroup (수평/수직)
|
||||
|
||||
**properties**: `{ layout: "horizontal" | "vertical" }`
|
||||
|
||||
**options**: `[{label, value}]`
|
||||
|
||||
---
|
||||
|
||||
### 3-1. ToggleField
|
||||
|
||||
**파일**: `DynamicItemForm/fields/ToggleField.tsx`
|
||||
|
||||
**역할**: On/Off 토글 스위치
|
||||
|
||||
**UI**: Switch + 라벨
|
||||
|
||||
**properties**: `{ onLabel, offLabel, onValue, offValue }`
|
||||
|
||||
---
|
||||
|
||||
### 3-2. ComputedField
|
||||
|
||||
**파일**: `DynamicItemForm/fields/ComputedField.tsx`
|
||||
|
||||
**역할**: 다른 필드 기반 자동 계산 (읽기 전용)
|
||||
|
||||
**UI**: 읽기전용 표시 (배경색 구분, muted)
|
||||
|
||||
**properties**: `{ formula, dependsOn, format, precision }`
|
||||
|
||||
**동작**: `dependsOn` 필드 값이 변경될 때마다 formula 재계산
|
||||
|
||||
---
|
||||
|
||||
## 파일 구조
|
||||
|
||||
```
|
||||
DynamicItemForm/
|
||||
├── fields/
|
||||
│ ├── DynamicFieldRenderer.tsx ← switch 확장
|
||||
│ ├── TextField.tsx (기존)
|
||||
│ ├── NumberField.tsx (기존)
|
||||
│ ├── DropdownField.tsx (기존)
|
||||
│ ├── CheckboxField.tsx (기존)
|
||||
│ ├── DateField.tsx (기존)
|
||||
│ ├── TextareaField.tsx (기존)
|
||||
│ ├── ReferenceField.tsx ★ Phase 1
|
||||
│ ├── MultiSelectField.tsx ★ Phase 1
|
||||
│ ├── FileField.tsx ★ Phase 1
|
||||
│ ├── CurrencyField.tsx ★ Phase 2
|
||||
│ ├── UnitValueField.tsx ★ Phase 2
|
||||
│ ├── RadioField.tsx ★ Phase 2
|
||||
│ ├── ToggleField.tsx ★ Phase 3
|
||||
│ └── ComputedField.tsx ★ Phase 3
|
||||
├── sections/
|
||||
│ ├── DynamicBOMSection.tsx (기존)
|
||||
│ ├── DynamicTableSection.tsx ★ Phase 1
|
||||
│ └── TableCellRenderer.tsx ★ Phase 1
|
||||
├── config/
|
||||
│ └── reference-sources.ts ★ Phase 1
|
||||
├── presets/
|
||||
│ └── section-presets.ts ★ Phase 2
|
||||
├── hooks/ (기존, 변경 없음)
|
||||
├── types.ts ← 타입 확장
|
||||
└── index.tsx ← table 섹션 렌더링 추가
|
||||
```
|
||||
|
||||
## 기존 코드 수정 범위
|
||||
|
||||
| 파일 | 수정 내용 | 줄 수 |
|
||||
|------|----------|-------|
|
||||
| `DynamicFieldRenderer.tsx` | switch case 8개 추가 + import | +20줄 |
|
||||
| `types.ts` | ExtendedFieldType union + config 인터페이스 | +80줄 |
|
||||
| `index.tsx` | 섹션 렌더링에 `case 'table'` 추가 | +15줄 |
|
||||
| `item-master-api.ts` | field_type union 확장 | +3줄 |
|
||||
|
||||
**기존 컴포넌트 6개 + BOM + hooks 7개 = 변경 없음**
|
||||
|
||||
---
|
||||
|
||||
## 상태 범례
|
||||
|
||||
- ⬜ 대기
|
||||
- 🔄 진행 중
|
||||
- ✅ 완료
|
||||
- ⏸️ 보류
|
||||
|
||||
---
|
||||
|
||||
**마지막 업데이트**: 2026-02-12 — Phase 1+2+3 전체 완료 (15/15 항목)
|
||||
@@ -0,0 +1,112 @@
|
||||
# Phase 1-4: 에러 메시지 포맷 통합 (`formatApiError` 제거)
|
||||
|
||||
> 난이도: 저 | 영향도: 🟡 API 레이어 정리 | 예상 변경: 1파일 삭제
|
||||
|
||||
---
|
||||
|
||||
## 현황 요약
|
||||
|
||||
에러 메시지 포맷팅 함수가 2곳에 중복:
|
||||
|
||||
| 파일 | 함수 | 외부 사용처 |
|
||||
|------|------|------------|
|
||||
| `src/lib/api/error-handler.ts:122` | `getErrorMessage()` | **5+ 파일** (활발히 사용) |
|
||||
| `src/lib/api/toast-utils.ts:106` | `formatApiError()` | **0건** (dead code) |
|
||||
|
||||
또한 `SHOW_ERROR_CODE` 상수도 양쪽에 중복 정의됨.
|
||||
|
||||
---
|
||||
|
||||
## 핵심 발견: toast-utils.ts 전체가 dead code
|
||||
|
||||
`from '@/lib/api/toast-utils'` 를 import하는 파일이 **0건**.
|
||||
|
||||
```
|
||||
toast-utils.ts 내보내는 함수 전부 미사용:
|
||||
- toastApiError() → 0 import
|
||||
- toastSuccess() → 0 import
|
||||
- toastWarning() → 0 import
|
||||
- toastInfo() → 0 import
|
||||
- formatApiError() → 0 import
|
||||
```
|
||||
|
||||
현재 프로젝트에서 에러 토스트 표시는 직접 `toast.error(getErrorMessage(err))` 패턴으로 처리 중.
|
||||
|
||||
---
|
||||
|
||||
## 작업 내역
|
||||
|
||||
### Step 1: `src/lib/api/toast-utils.ts` 삭제
|
||||
|
||||
파일 전체가 dead code이므로 삭제.
|
||||
|
||||
### Step 2: (선택) 유용한 헬퍼를 error-handler.ts로 이동
|
||||
|
||||
`toastApiError()` 함수는 validation 에러의 첫 번째 필드를 표시하는 로직이 있어,
|
||||
향후 유용할 수 있으면 error-handler.ts 하단에 통합 가능.
|
||||
|
||||
```typescript
|
||||
// src/lib/api/error-handler.ts 하단에 추가 (선택)
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function toastApiError(error: unknown, fallbackMessage = '오류가 발생했습니다.'): void {
|
||||
if (error instanceof ApiError && error.errors && SHOW_ERROR_CODE) {
|
||||
const firstField = Object.keys(error.errors)[0];
|
||||
if (firstField) {
|
||||
toast.error(`${getErrorMessage(error)}\n${firstField}: ${error.errors[firstField][0]}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
toast.error(getErrorMessage(error) || fallbackMessage);
|
||||
}
|
||||
```
|
||||
|
||||
이 step은 **선택**. 현재 사용처가 없으므로 당장은 삭제만으로 충분.
|
||||
|
||||
### Step 3: 검증
|
||||
|
||||
```bash
|
||||
npx tsc --noEmit
|
||||
```
|
||||
|
||||
toast-utils.ts를 삭제해도 외부 import가 없으므로 타입 에러 없음.
|
||||
|
||||
---
|
||||
|
||||
## 관련 파일 참조
|
||||
|
||||
### 활발히 사용 중인 함수 (변경 없음)
|
||||
|
||||
`getErrorMessage()` 사용처 (error-handler.ts에서 export):
|
||||
- `src/contexts/ItemMasterContext.tsx` (line 7, 589, 682)
|
||||
- `src/components/items/ItemMasterDataManagement/hooks/usePageManagement.ts` (line 7, 122, 159, 198, 219)
|
||||
- `src/components/items/ItemMasterDataManagement/hooks/useImportManagement.ts` (line 5, 58, 80, 92)
|
||||
- `src/components/items/ItemMasterDataManagement/hooks/useInitialDataLoading.ts` (line 7, 130)
|
||||
- `src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx` (line 40, 301, 347)
|
||||
|
||||
### 삭제 대상
|
||||
|
||||
- `src/lib/api/toast-utils.ts` (전체 116줄)
|
||||
|
||||
---
|
||||
|
||||
## 중복 구조 비교
|
||||
|
||||
```
|
||||
error-handler.ts toast-utils.ts (삭제 대상)
|
||||
───────────────── ──────────────────────────
|
||||
const SHOW_ERROR_CODE = true; const SHOW_ERROR_CODE = true; ← 중복
|
||||
|
||||
getErrorMessage(error): formatApiError(error):
|
||||
DuplicateCodeError → [status] ApiError → [status] msg
|
||||
ApiError → [status] msg else → getErrorMessage() ← 결국 위임
|
||||
Error → .message
|
||||
unknown → 기본 메시지
|
||||
toastApiError(error):
|
||||
DuplicateCodeError → toast ← getErrorMessage와 동일 로직
|
||||
ApiError → toast
|
||||
Error → toast
|
||||
unknown → toast
|
||||
```
|
||||
|
||||
`formatApiError`는 결국 `getErrorMessage`를 호출하는 래퍼에 불과. 삭제해도 기능 손실 없음.
|
||||
@@ -0,0 +1,229 @@
|
||||
# Phase 1-5: Zustand 셀렉터 훅 추가 (3개 스토어)
|
||||
|
||||
> 난이도: 저 | 영향도: 🟡 리렌더 최적화 | 예상 변경: 3 스토어 + 4 컨슈머
|
||||
|
||||
---
|
||||
|
||||
## 현황 요약
|
||||
|
||||
셀렉터 없이 전체 스토어를 구독하면, 무관한 상태 변경에도 컴포넌트가 리렌더됩니다.
|
||||
|
||||
| 스토어 | 셀렉터 훅 | 사용처 | 문제 |
|
||||
|--------|----------|--------|------|
|
||||
| ✅ `masterDataStore` | `usePageConfig()` 등 | 다수 | 양호 |
|
||||
| ✅ `authStore` | `useCurrentUser()` 등 | 4곳 | 양호 (방금 추가) |
|
||||
| ❌ `useTableColumnStore` | 없음 | 1곳 | 전체 스토어 구독 |
|
||||
| ❌ `useMenuStore` | 없음 | 15곳 | 일부 전체 구독 |
|
||||
| ❌ `useThemeStore` | 없음 | 2곳 | 전체 구독 |
|
||||
|
||||
---
|
||||
|
||||
## 작업 내역
|
||||
|
||||
### Step 1: `src/stores/useTableColumnStore.ts` — 셀렉터 훅 추가
|
||||
|
||||
파일 끝에 추가:
|
||||
|
||||
```typescript
|
||||
// ===== 셀렉터 훅 =====
|
||||
|
||||
/** 특정 페이지의 컬럼 설정만 구독 */
|
||||
export const usePageColumnSettings = (pageId: string) =>
|
||||
useTableColumnStore((state) => state.pageSettings[pageId] ?? DEFAULT_PAGE_SETTINGS);
|
||||
|
||||
/** 특정 페이지의 숨김 컬럼만 구독 */
|
||||
export const useHiddenColumns = (pageId: string) =>
|
||||
useTableColumnStore((state) => state.pageSettings[pageId]?.hiddenColumns ?? []);
|
||||
|
||||
/** 특정 페이지의 컬럼 너비만 구독 */
|
||||
export const useColumnWidths = (pageId: string) =>
|
||||
useTableColumnStore((state) => state.pageSettings[pageId]?.columnWidths ?? {});
|
||||
```
|
||||
|
||||
**주의**: `DEFAULT_PAGE_SETTINGS` 객체는 파일 내에 이미 정의되어 있음 (line 30-33).
|
||||
|
||||
**컨슈머 변경** — `src/hooks/useColumnSettings.ts`:
|
||||
|
||||
```typescript
|
||||
// Before (line 17)
|
||||
const store = useTableColumnStore(); // 전체 스토어 구독
|
||||
const settings = store.getPageSettings(pageId);
|
||||
|
||||
// After
|
||||
const settings = usePageColumnSettings(pageId); // 해당 페이지 설정만 구독
|
||||
const { setColumnWidth: storeSetWidth, toggleColumnVisibility: storeToggle, resetPageSettings } = useTableColumnStore.getState();
|
||||
// 또는 액션만 별도 구독 (액션은 참조 안정적이라 리렌더 유발 안 함):
|
||||
const setColumnWidth = useTableColumnStore((s) => s.setColumnWidth);
|
||||
const toggleColumnVisibility = useTableColumnStore((s) => s.toggleColumnVisibility);
|
||||
const resetPageSettings = useTableColumnStore((s) => s.resetPageSettings);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: `src/stores/menuStore.ts` — 셀렉터 훅 추가
|
||||
|
||||
파일 끝에 추가:
|
||||
|
||||
```typescript
|
||||
// ===== 셀렉터 훅 =====
|
||||
|
||||
/** 사이드바 접힘 상태만 구독 */
|
||||
export const useSidebarCollapsed = () =>
|
||||
useMenuStore((state) => state.sidebarCollapsed);
|
||||
|
||||
/** 활성 메뉴 ID만 구독 */
|
||||
export const useActiveMenu = () =>
|
||||
useMenuStore((state) => state.activeMenu);
|
||||
|
||||
/** 메뉴 아이템 목록만 구독 */
|
||||
export const useMenuItems = () =>
|
||||
useMenuStore((state) => state.menuItems);
|
||||
|
||||
/** 하이드레이션 완료 여부만 구독 */
|
||||
export const useMenuHydrated = () =>
|
||||
useMenuStore((state) => state._hasHydrated);
|
||||
```
|
||||
|
||||
**컨슈머 변경 대상**:
|
||||
|
||||
#### 2-A. `src/layouts/AuthenticatedLayout.tsx` (line 99) — 🔴 핵심
|
||||
|
||||
현재: 전체 스토어 디스트럭처링
|
||||
```typescript
|
||||
const { menuItems, activeMenu, setActiveMenu, setMenuItems, sidebarCollapsed, toggleSidebar, _hasHydrated } = useMenuStore();
|
||||
```
|
||||
|
||||
변경:
|
||||
```typescript
|
||||
const menuItems = useMenuItems();
|
||||
const activeMenu = useActiveMenu();
|
||||
const sidebarCollapsed = useSidebarCollapsed();
|
||||
const _hasHydrated = useMenuHydrated();
|
||||
// 액션은 참조 안정적이므로 별도 셀렉터:
|
||||
const setActiveMenu = useMenuStore((s) => s.setActiveMenu);
|
||||
const setMenuItems = useMenuStore((s) => s.setMenuItems);
|
||||
const toggleSidebar = useMenuStore((s) => s.toggleSidebar);
|
||||
```
|
||||
|
||||
#### 2-B. `src/components/production/WorkerScreen/index.tsx` (line 327)
|
||||
|
||||
현재:
|
||||
```typescript
|
||||
const { sidebarCollapsed } = useMenuStore(); // 전체 구독
|
||||
```
|
||||
|
||||
변경:
|
||||
```typescript
|
||||
const sidebarCollapsed = useSidebarCollapsed();
|
||||
```
|
||||
|
||||
#### 2-C. `src/components/layout/CommandMenuSearch.tsx` (line 68)
|
||||
|
||||
현재:
|
||||
```typescript
|
||||
const { menuItems } = useMenuStore(); // 전체 구독
|
||||
```
|
||||
|
||||
변경:
|
||||
```typescript
|
||||
const menuItems = useMenuItems();
|
||||
```
|
||||
|
||||
#### 2-D. 나머지 sidebarCollapsed 사용 파일 (이미 셀렉터 패턴)
|
||||
|
||||
아래 파일들은 이미 `useMenuStore((state) => state.sidebarCollapsed)` 패턴을 사용 중이므로 **변경 불필요**:
|
||||
- `ItemDetail.tsx`, `ChecklistDetail.tsx`, `PriceDistributionDetail.tsx`
|
||||
- `StepDetail.tsx`, `PermissionDetailClient.tsx`, `BoardDetail/index.tsx`
|
||||
- `ProcessDetail.tsx`, `PricingTableForm.tsx`, `DynamicItemForm/index.tsx`
|
||||
- `ItemDetailClient.tsx`, `ClientDetail.tsx`, `DetailActions.tsx`
|
||||
|
||||
단, 셀렉터 훅이 추가되면 이 파일들도 향후 `useSidebarCollapsed()`로 전환 가능 (선택).
|
||||
|
||||
---
|
||||
|
||||
### Step 3: `src/stores/themeStore.ts` — 셀렉터 훅 추가
|
||||
|
||||
파일 끝에 추가:
|
||||
|
||||
```typescript
|
||||
// ===== 셀렉터 훅 =====
|
||||
|
||||
/** 현재 테마만 구독 */
|
||||
export const useTheme = () =>
|
||||
useThemeStore((state) => state.theme);
|
||||
|
||||
/** setTheme 액션만 구독 */
|
||||
export const useSetTheme = () =>
|
||||
useThemeStore((state) => state.setTheme);
|
||||
```
|
||||
|
||||
**컨슈머 변경 대상**:
|
||||
|
||||
#### 3-A. `src/layouts/AuthenticatedLayout.tsx` (line 100)
|
||||
|
||||
현재:
|
||||
```typescript
|
||||
const { theme, setTheme } = useThemeStore();
|
||||
```
|
||||
|
||||
변경:
|
||||
```typescript
|
||||
const theme = useTheme();
|
||||
const setTheme = useSetTheme();
|
||||
```
|
||||
|
||||
#### 3-B. `src/components/ThemeSelect.tsx` (line 24)
|
||||
|
||||
현재:
|
||||
```typescript
|
||||
const { theme, setTheme } = useThemeStore();
|
||||
```
|
||||
|
||||
변경:
|
||||
```typescript
|
||||
const theme = useTheme();
|
||||
const setTheme = useSetTheme();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 검증
|
||||
|
||||
```bash
|
||||
npx tsc --noEmit
|
||||
```
|
||||
|
||||
셀렉터 훅은 기존 API에 추가만 하는 것이므로 기존 코드에 영향 없음.
|
||||
컨슈머 변경은 import 경로와 호출 패턴만 바뀌므로 타입 에러 가능성 낮음.
|
||||
|
||||
---
|
||||
|
||||
## 변경 파일 총 정리
|
||||
|
||||
| # | 파일 | 작업 | 내용 |
|
||||
|---|------|------|------|
|
||||
| 1 | `src/stores/useTableColumnStore.ts` | 추가 | 셀렉터 훅 3개 (`usePageColumnSettings`, `useHiddenColumns`, `useColumnWidths`) |
|
||||
| 2 | `src/stores/menuStore.ts` | 추가 | 셀렉터 훅 4개 (`useSidebarCollapsed`, `useActiveMenu`, `useMenuItems`, `useMenuHydrated`) |
|
||||
| 3 | `src/stores/themeStore.ts` | 추가 | 셀렉터 훅 2개 (`useTheme`, `useSetTheme`) |
|
||||
| 4 | `src/hooks/useColumnSettings.ts` | 수정 | `useTableColumnStore()` → 셀렉터 패턴 |
|
||||
| 5 | `src/layouts/AuthenticatedLayout.tsx` | 수정 | menuStore/themeStore 전체 구독 → 셀렉터 |
|
||||
| 6 | `src/components/production/WorkerScreen/index.tsx` | 수정 | `useMenuStore()` → `useSidebarCollapsed()` |
|
||||
| 7 | `src/components/layout/CommandMenuSearch.tsx` | 수정 | `useMenuStore()` → `useMenuItems()` |
|
||||
| 8 | `src/components/ThemeSelect.tsx` | 수정 | `useThemeStore()` → `useTheme()` + `useSetTheme()` |
|
||||
|
||||
---
|
||||
|
||||
## 참고: Zustand 셀렉터가 중요한 이유
|
||||
|
||||
```
|
||||
// ❌ 전체 구독 — menuItems 변경 시 sidebarCollapsed만 쓰는 컴포넌트도 리렌더
|
||||
const { sidebarCollapsed } = useMenuStore();
|
||||
|
||||
// ✅ 셀렉터 — sidebarCollapsed 변경 시에만 리렌더
|
||||
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
|
||||
// 또는
|
||||
const sidebarCollapsed = useSidebarCollapsed();
|
||||
```
|
||||
|
||||
Zustand는 `Object.is`로 반환값을 비교. 셀렉터가 원시값(string, boolean, number)을 반환하면 참조 비교로 정확히 변경 감지.
|
||||
객체를 반환하는 셀렉터(예: `usePageColumnSettings`)는 같은 참조를 반환하므로 해당 pageId의 설정이 변경될 때만 리렌더.
|
||||
@@ -0,0 +1,254 @@
|
||||
# IntegratedDetailTemplate 마이그레이션 체크리스트
|
||||
|
||||
> 최종 수정: 2026-01-21
|
||||
> 브랜치: `feature/universal-detail-component`
|
||||
|
||||
---
|
||||
|
||||
## 📊 전체 진행 현황
|
||||
|
||||
| 단계 | 내용 | 상태 | 대상 |
|
||||
|------|------|------|------|
|
||||
| **Phase 1-5** | V2 URL 패턴 통합 | ✅ 완료 | 37개 |
|
||||
| **Phase 6** | 폼 템플릿 공통화 | ✅ 완료 | 41개 |
|
||||
|
||||
### 통계 요약
|
||||
|
||||
| 구분 | 개수 |
|
||||
|------|------|
|
||||
| ✅ V2 URL 패턴 완료 | 37개 |
|
||||
| ✅ IntegratedDetailTemplate 적용 완료 | 41개 |
|
||||
| ❌ 제외 (특수 레이아웃) | 10개 |
|
||||
| ⚪ 불필요 (View only 등) | 8개 |
|
||||
|
||||
---
|
||||
|
||||
## 📌 V2 URL 패턴이란?
|
||||
|
||||
```
|
||||
기존: /[id] (조회) + /[id]/edit (수정) → 별도 페이지
|
||||
V2: /[id]?mode=view (조회) + /[id]?mode=edit (수정) → 단일 페이지
|
||||
```
|
||||
|
||||
**핵심**: `searchParams.get('mode')` 로 view/edit 분기
|
||||
|
||||
---
|
||||
|
||||
## 🎯 마이그레이션 목표
|
||||
|
||||
- **타이틀/버튼 영역** (목록, 상세, 취소, 수정) 공통화
|
||||
- **반응형 입력 필드** 통합
|
||||
- **특수 기능** (테이블, 모달, 문서 미리보기 등)은 `renderView`/`renderForm`으로 유지
|
||||
- **한 파일 수정으로 전체 페이지 일괄 적용** 가능
|
||||
|
||||
---
|
||||
|
||||
## 🔧 마이그레이션 패턴 가이드
|
||||
|
||||
### Pattern 1: Config 기반 템플릿
|
||||
|
||||
```typescript
|
||||
// 1. config 파일 생성
|
||||
export const xxxConfig: DetailConfig = {
|
||||
title: '페이지 타이틀',
|
||||
description: '설명',
|
||||
icon: IconComponent,
|
||||
basePath: '/path/to/list',
|
||||
fields: [], // renderView/renderForm 사용 시 빈 배열
|
||||
gridColumns: 2,
|
||||
actions: {
|
||||
showBack: true,
|
||||
showDelete: true,
|
||||
showEdit: true,
|
||||
showSave: true, // false로 설정하면 기본 저장 버튼 숨김
|
||||
submitLabel: '저장',
|
||||
cancelLabel: '취소',
|
||||
},
|
||||
};
|
||||
|
||||
// 2. 컴포넌트에서 IntegratedDetailTemplate 사용
|
||||
<IntegratedDetailTemplate
|
||||
config={dynamicConfig}
|
||||
mode={mode}
|
||||
initialData={data}
|
||||
itemId={id}
|
||||
isLoading={isLoading}
|
||||
onSubmit={handleSubmit} // Promise<{ success: boolean; error?: string }>
|
||||
onDelete={handleDelete} // Promise<{ success: boolean; error?: string }>
|
||||
headerActions={customHeaderActions} // 커스텀 버튼
|
||||
renderView={() => renderContent()}
|
||||
renderForm={() => renderContent()}
|
||||
/>
|
||||
```
|
||||
|
||||
### Pattern 2: View/Edit 컴포넌트 분리
|
||||
|
||||
```tsx
|
||||
// View와 Edit가 완전히 다른 구현인 경우
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
|
||||
if (mode === 'edit') {
|
||||
return <ComponentDetailEdit id={id} />;
|
||||
}
|
||||
return <ComponentDetailView id={id} />;
|
||||
```
|
||||
|
||||
### Pattern 3: 커스텀 버튼이 필요한 경우
|
||||
|
||||
```tsx
|
||||
// config에서 showSave: false 설정
|
||||
// headerActions prop으로 커스텀 버튼 전달
|
||||
<IntegratedDetailTemplate
|
||||
config={{ ...config, actions: { ...config.actions, showSave: false } }}
|
||||
headerActions={
|
||||
<>
|
||||
<Button onClick={handlePreview}>미리보기</Button>
|
||||
<Button onClick={handleSubmit}>상신</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 6 적용 완료 (41개)
|
||||
|
||||
| No | 카테고리 | 컴포넌트 | 파일 | 특이사항 |
|
||||
|----|---------|---------|------|----------|
|
||||
| 1 | 건설 | 협력업체 | PartnerForm.tsx | - |
|
||||
| 2 | 건설 | 시공관리 | ConstructionDetailClient.tsx | - |
|
||||
| 3 | 건설 | 기성관리 | ProgressBillingDetailForm.tsx | - |
|
||||
| 4 | 건설 | 발주관리 | OrderDetailForm.tsx | - |
|
||||
| 5 | 건설 | 계약관리 | ContractDetailForm.tsx | - |
|
||||
| 6 | 건설 | 인수인계보고서 | HandoverReportDetailForm.tsx | - |
|
||||
| 7 | 건설 | 견적관리 | EstimateDetailForm.tsx | - |
|
||||
| 8 | 건설 | 현장브리핑 | SiteBriefingForm.tsx | - |
|
||||
| 9 | 건설 | 이슈관리 | IssueDetailForm.tsx | - |
|
||||
| 10 | 건설 | 입찰관리 | BiddingDetailForm.tsx | - |
|
||||
| 11 | 건설 | 구조검토 | StructureReviewDetailForm.tsx | view/edit/new 모드, 파일 드래그앤드롭 |
|
||||
| 12 | 건설 | 현장관리 | SiteDetailForm.tsx | 다음 우편번호 API, 파일 드래그앤드롭 |
|
||||
| 13 | 건설 | 품목관리 | ItemDetailClient.tsx | view/edit/new 모드, 동적 발주 항목 리스트 |
|
||||
| 14 | 영업 | 견적관리(V2) | QuoteRegistrationV2.tsx | hideHeader prop, 자동견적/푸터바 유지 |
|
||||
| 15 | 영업 | 고객관리(V2) | ClientDetailClientV2.tsx | - |
|
||||
| 16 | 영업 | 수주관리 | OrderSalesDetailView/Edit.tsx | 문서 모달, 상태별 버튼, 확정/취소 다이얼로그 |
|
||||
| 17 | 회계 | 청구관리 | BillDetail.tsx | - |
|
||||
| 18 | 회계 | 매입관리 | PurchaseDetail.tsx | - |
|
||||
| 19 | 회계 | 매출관리 | SalesDetail.tsx | - |
|
||||
| 20 | 회계 | 거래처관리 | VendorDetail.tsx | - |
|
||||
| 21 | 회계 | 입금관리(V2) | DepositDetailClientV2.tsx | - |
|
||||
| 22 | 회계 | 출금관리(V2) | WithdrawalDetailClientV2.tsx | - |
|
||||
| 23 | 회계 | 악성채권 | BadDebtDetail.tsx | 저장 확인 다이얼로그, 파일 업로드/다운로드 |
|
||||
| 24 | 회계 | 거래처원장 | VendorLedgerDetail.tsx | 기간선택, PDF 다운로드, 판매/수금 테이블 |
|
||||
| 25 | 생산 | 작업지시 | WorkOrderDetail.tsx | 상태변경버튼, 작업일지 모달 유지 |
|
||||
| 26 | 품질 | 검수관리 | InspectionDetail.tsx | 성적서 버튼 |
|
||||
| 27 | 출고 | 출하관리 | ShipmentDetail.tsx | 문서 미리보기 모달, 조건부 수정/삭제 |
|
||||
| 28 | 자재 | 입고관리 | ReceivingDetail.tsx | 입고증/입고처리/성공 다이얼로그, 상태별 버튼 |
|
||||
| 29 | 자재 | 재고현황 | StockStatusDetail.tsx | LOT별 상세 재고 테이블, FIFO 권장 메시지 |
|
||||
| 30 | 기준정보 | 단가관리(V2) | PricingDetailClientV2.tsx | - |
|
||||
| 31 | 기준정보 | 노무관리(V2) | LaborDetailClientV2.tsx | - |
|
||||
| 32 | 설정 | 팝업관리(V2) | PopupDetailClientV2.tsx | - |
|
||||
| 33 | 설정 | 계정관리 | accounts/[id]/page.tsx | - |
|
||||
| 34 | 설정 | 공정관리 | process-management/[id]/page.tsx | - |
|
||||
| 35 | 설정 | 게시판관리 | board-management/[id]/page.tsx | - |
|
||||
| 36 | 설정 | 권한관리 | PermissionDetail.tsx | 인라인 수정, 메뉴별 권한 테이블, 자동 저장 |
|
||||
| 37 | 인사 | 명함관리 | card-management/[id]/page.tsx | - |
|
||||
| 38 | 인사 | 직원관리 | EmployeeDetail.tsx | 기본정보/인사정보/사용자정보 카드 |
|
||||
| 39 | 고객센터 | 문의관리 | InquiryDetail.tsx | 댓글 CRUD, 작성자/상태별 버튼 표시 |
|
||||
| 40 | 고객센터 | 이벤트관리 | EventDetail.tsx | view 모드만 |
|
||||
| 41 | 고객센터 | 공지관리 | NoticeDetail.tsx | view 모드만, 이미지/첨부파일 |
|
||||
|
||||
---
|
||||
|
||||
## 📋 등록/수정 페이지 마이그레이션 (Phase 1-8)
|
||||
|
||||
### Phase 1 - 기안함
|
||||
- [x] DocumentCreate (기안함 등록/수정)
|
||||
- 파일: `src/components/approval/DocumentCreate/index.tsx`
|
||||
- 특이사항: 커스텀 headerActions (미리보기, 삭제, 상신, 임시저장)
|
||||
|
||||
### Phase 2 - 생산관리
|
||||
- [x] WorkOrderCreate/Edit (작업지시 등록/수정)
|
||||
- 파일: `src/components/production/WorkOrders/WorkOrderCreate.tsx`
|
||||
|
||||
### Phase 3 - 출고관리
|
||||
- [x] ShipmentCreate/Edit (출하 등록/수정)
|
||||
- 파일: `src/components/outbound/ShipmentManagement/ShipmentCreate.tsx`
|
||||
|
||||
### Phase 4 - HR
|
||||
- [x] EmployeeForm (사원 등록/수정/상세)
|
||||
- 파일: `src/components/hr/EmployeeManagement/EmployeeForm.tsx`
|
||||
- 특이사항: "항목 설정" 버튼, 복잡한 섹션 구조
|
||||
|
||||
### Phase 5 - 게시판
|
||||
- [x] BoardForm (게시판 글쓰기/수정)
|
||||
- 파일: `src/components/board/BoardForm/index.tsx`
|
||||
|
||||
### Phase 6 - 고객센터
|
||||
- [x] InquiryForm (문의 등록/수정)
|
||||
- 파일: `src/components/customer-center/InquiryManagement/InquiryForm.tsx`
|
||||
|
||||
### Phase 7 - 기준정보
|
||||
- [x] ProcessForm (공정 등록/수정)
|
||||
- 파일: `src/components/process-management/ProcessForm.tsx`
|
||||
|
||||
### Phase 8 - 자재/품질
|
||||
- [x] InspectionCreate - 자재 (수입검사 등록)
|
||||
- [x] InspectionCreate - 품질 (품질검사 등록)
|
||||
|
||||
---
|
||||
|
||||
## ❌ 마이그레이션 제외 (특수 레이아웃)
|
||||
|
||||
| 페이지 | 경로 | 사유 |
|
||||
|--------|------|------|
|
||||
| CEO 대시보드 | - | 대시보드 (특수 레이아웃) |
|
||||
| 생산 대시보드 | - | 대시보드 (특수 레이아웃) |
|
||||
| 작업자 화면 | - | 특수 UI |
|
||||
| 설정 페이지들 | - | 트리 구조, 특수 레이아웃 |
|
||||
| 부서 관리 | - | 트리 구조 |
|
||||
| 일일보고서 | - | 특수 레이아웃 |
|
||||
| 미수금현황 | - | 특수 레이아웃 |
|
||||
| 종합분석 | - | 특수 레이아웃 |
|
||||
| 현장종합현황 | `/construction/project/management/[id]` | 칸반 보드 |
|
||||
| 권한관리 | `/settings/permissions/[id]` | Matrix UI |
|
||||
|
||||
---
|
||||
|
||||
## 📚 Config 파일 위치 참조
|
||||
|
||||
| 컴포넌트 | Config 파일 |
|
||||
|---------|------------|
|
||||
| 출하관리 | shipmentConfig.ts |
|
||||
| 작업지시 | workOrderConfig.ts |
|
||||
| 검수관리 | inspectionConfig.ts |
|
||||
| 견적관리(V2) | quoteConfig.ts |
|
||||
| 수주관리 | orderSalesConfig.ts |
|
||||
| 입고관리 | receivingConfig.ts |
|
||||
| 재고현황 | stockStatusConfig.ts |
|
||||
| 악성채권 | badDebtConfig.ts |
|
||||
| 거래처원장 | vendorLedgerConfig.ts |
|
||||
| 구조검토 | structureReviewConfig.ts |
|
||||
| 현장관리 | siteConfig.ts |
|
||||
| 품목관리 | itemConfig.ts |
|
||||
| 문의관리 | inquiryConfig.ts |
|
||||
| 이벤트관리 | eventConfig.ts |
|
||||
| 공지관리 | noticeConfig.ts |
|
||||
| 직원관리 | employeeConfig.ts |
|
||||
| 권한관리 | permissionConfig.ts |
|
||||
|
||||
---
|
||||
|
||||
## 📝 변경 이력
|
||||
|
||||
<details>
|
||||
<summary>전체 변경 이력 보기</summary>
|
||||
|
||||
| 날짜 | 내용 |
|
||||
|------|------|
|
||||
| 2026-01-17 | 체크리스트 초기 작성 |
|
||||
| 2026-01-19 | Phase 1-5 V2 URL 패턴 마이그레이션 완료 (37개) |
|
||||
| 2026-01-20 | Phase 6 폼 템플릿 공통화 마이그레이션 완료 (41개) |
|
||||
| 2026-01-20 | 기안함, 작업지시, 출하, 사원, 게시판, 문의, 공정, 검사 마이그레이션 완료 |
|
||||
| 2026-01-21 | 문서 통합 (중복 3개 파일 → 1개) |
|
||||
|
||||
</details>
|
||||
@@ -0,0 +1,74 @@
|
||||
# SAM ERP 프론트엔드 개선 로드맵
|
||||
|
||||
> 작성일: 2025-02-10
|
||||
> 분석 기준: src/ 전체 (500+ 파일, ~163K줄)
|
||||
|
||||
---
|
||||
|
||||
## Phase A: 즉시 개선 — ✅ 완료
|
||||
|
||||
| # | 항목 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| A-1 | `<img>` → `next/image` 전환 | ✅ **전환 불필요 결정** | 폐쇄형 ERP, 전량 외부 동적 이미지, blob URL 비호환 (`_index.md` 참조) |
|
||||
| A-2 | DataTable 렌더링 최적화 | ⏳ 대기 | TableRow memo + useCallback |
|
||||
|
||||
---
|
||||
|
||||
## Phase B: 단기 개선 — ✅ 완료
|
||||
|
||||
| # | 항목 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| B-1 | `next/dynamic` 코드 스플리팅 | ✅ **완료** | 대시보드 4개 + xlsx 동적 로드, ~850KB 절감 (`_index.md` 참조) |
|
||||
| B-2 | API 병렬 호출 (`Promise.all`) | ✅ **완료** | |
|
||||
| B-3 | `store/` vs `stores/` 통합 | ✅ **완료** | |
|
||||
|
||||
---
|
||||
|
||||
## Phase C: 중기 개선 — ✅ 완료
|
||||
|
||||
| # | 항목 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| C-1 | 테이블 가상화 (react-window) | ✅ **보류 결정** | 페이지네이션 사용 중, YAGNI (`_index.md` 참조) |
|
||||
| C-2 | SWR / React Query | ✅ **보류 결정** | Zustand 캐싱 충족 (`_index.md` 참조) |
|
||||
| C-3 | Action 팩토리 패턴 확대 | ✅ **규칙 확정** | 신규 CRUD만 팩토리 사용 (`_index.md` 참조) |
|
||||
| C-4 | V1/V2 컴포넌트 정리 | ✅ **완료** | V2 최종본 확정, V1 삭제, 접미사 제거 |
|
||||
|
||||
---
|
||||
|
||||
## Phase D: 장기 개선 (필요 시)
|
||||
|
||||
| # | 항목 | 상태 |
|
||||
|---|------|------|
|
||||
| D-1 | God 컴포넌트 분리 (5개, 1200~2700줄) | ⏳ 대기 |
|
||||
| D-2 | `as` 타입 캐스트 점진적 제거 (926건) | ✅ **보류 결정** | 실제 ~200건만 actionable, 신규 코드에서 제네릭 활용 (2026-02-11) |
|
||||
| D-3 | `@deprecated` 함수 정리 (13파일) | ✅ **즉시 삭제분 완료** | uploadFile/deleteFile/getSiteNames/deprecated props 삭제 (2026-02-11) |
|
||||
| D-4 | Molecules 레이어 활성화 | ✅ **보류 결정** | 사용률 ~0%, organisms/templates로 충분 (2026-02-11) |
|
||||
| D-5 | 모달 컴포넌트 통합 | ✅ **완료** | InspectionPreviewModal → DocumentViewer 전환 (2026-02-11) |
|
||||
| D-6 | 기타 (TODO 102건, strictMode 등) | ⏳ 대기 |
|
||||
|
||||
---
|
||||
|
||||
## 이전 리팩토링 완료 항목 (참고)
|
||||
|
||||
| 항목 | 상태 | 날짜 |
|
||||
|------|------|------|
|
||||
| Phase 1: 공통 훅 추출 (executeServerAction 등) | ✅ 완료 | 이전 세션 |
|
||||
| 중복 코드 공통화 (buildApiUrl 전체 43개 actions.ts 마이그레이션) | ✅ 완료 | 2026-02-12 |
|
||||
| executePaginatedAction 전체 마이그레이션 (14개 actions.ts, ~220줄 감소) | ✅ 완료 | 2026-02-12 |
|
||||
| Phase 3: 공용 유틸 추출 (PaginatedApiResponse 등) | ✅ 완료 | 이전 세션 |
|
||||
| Phase 4: SearchableSelectionModal 공통화 | ✅ 완료 | 이전 세션 |
|
||||
| Phase 5: any 21건 + memo 3개 정리 | ✅ 완료 | 이전 세션 |
|
||||
| console.log 524건 → 22건 정리 | ✅ 완료 | 2025-02-10 |
|
||||
| TODO 주석 정리 (login route) | ✅ 완료 | 2025-02-10 |
|
||||
| SSR 가드 추가 (ThemeContext, ApiErrorContext, useDetailPageState) | ✅ 완료 | 2025-02-10 |
|
||||
| 커스텀 훅 불필요 'use client' 15개 제거 | ✅ 완료 | 2025-02-10 |
|
||||
| formatDate 이름 충돌 해소 → formatCalendarDate | ✅ 완료 | 2025-02-10 |
|
||||
|
||||
---
|
||||
|
||||
## 우선순위 요약
|
||||
|
||||
```
|
||||
Phase A~C: ✅ 전체 완료/결정 완료 (A-2 DataTable 최적화만 대기)
|
||||
Phase D: 남은 작업 → A-2, D-1, D-6
|
||||
```
|
||||
@@ -0,0 +1,346 @@
|
||||
# 동적 메뉴 갱신 시스템
|
||||
|
||||
## 개요
|
||||
|
||||
관리자가 게시판/메뉴를 추가하면 사용자가 **재로그인 없이** 즉시 메뉴를 갱신받을 수 있는 시스템 구현.
|
||||
|
||||
## 현재 문제점
|
||||
|
||||
```
|
||||
현재 흐름:
|
||||
로그인 → API 응답에서 메뉴 수신 → localStorage.user.menu 저장 → 세션 종료까지 고정
|
||||
|
||||
문제:
|
||||
- 관리자가 게시판 추가해도 사용자는 재로그인 전까지 새 메뉴 안 보임
|
||||
- 메뉴 전용 갱신 API 없음
|
||||
- 실시간 알림 메커니즘 없음
|
||||
```
|
||||
|
||||
## 데이터 흐름 (현재)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 로그인 시 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ POST /api/v1/login │
|
||||
│ ↓ │
|
||||
│ 응답: { user, tenant, roles, menus } │
|
||||
│ ↓ │
|
||||
│ transformApiMenusToMenuItems(menus) │
|
||||
│ ↓ │
|
||||
│ localStorage.setItem('user', { ...userData, menu }) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 페이지 로드 시 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ AuthenticatedLayout.tsx │
|
||||
│ ↓ │
|
||||
│ localStorage.getItem('user') → userData.menu │
|
||||
│ ↓ │
|
||||
│ deserializeMenuItems(userData.menu) │
|
||||
│ ↓ │
|
||||
│ menuStore.setMenuItems(deserializedMenus) │
|
||||
│ ↓ │
|
||||
│ Sidebar 컴포넌트 렌더링 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 관련 파일
|
||||
|
||||
| 파일 | 역할 |
|
||||
|------|------|
|
||||
| `src/store/menuStore.ts` | Zustand 메뉴 상태 관리 |
|
||||
| `src/lib/utils/menuTransform.ts` | API 메뉴 → UI 메뉴 변환 |
|
||||
| `src/lib/utils/menuRefresh.ts` | 메뉴 갱신 유틸리티 (해시 비교, localStorage/Zustand 동시 업데이트) |
|
||||
| `src/hooks/useMenuPolling.ts` | 메뉴 폴링 훅 (30초 간격, 탭 가시성, 세션 만료 처리) |
|
||||
| `src/layouts/AuthenticatedLayout.tsx` | 메뉴 로드 및 스토어 설정 |
|
||||
| `src/components/layout/Sidebar.tsx` | 메뉴 렌더링 |
|
||||
| `src/contexts/AuthContext.tsx` | 사용자 인증 컨텍스트 |
|
||||
|
||||
---
|
||||
|
||||
## 구현 계획
|
||||
|
||||
### 1단계: 폴링 방식 (현재 구현 목표)
|
||||
|
||||
**방식**: 30초마다 메뉴 API 호출하여 변경사항 확인
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 폴링 방식 흐름 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ [30초마다] │
|
||||
│ ↓ │
|
||||
│ GET /api/menus (메뉴 전용 API 필요) │
|
||||
│ ↓ │
|
||||
│ 현재 메뉴와 비교 (해시 또는 버전 비교) │
|
||||
│ ↓ │
|
||||
│ 변경 있으면 → refreshMenus() 호출 │
|
||||
│ ↓ │
|
||||
│ localStorage.user.menu 업데이트 │
|
||||
│ menuStore.setMenuItems() 호출 │
|
||||
│ ↓ │
|
||||
│ UI 즉시 반영 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**장점**:
|
||||
- 구현 단순
|
||||
- 백엔드 수정 최소화 (메뉴 조회 API만 추가)
|
||||
- 기존 인프라 그대로 사용
|
||||
|
||||
**단점**:
|
||||
- 최대 30초 지연
|
||||
- 불필요한 API 호출 발생
|
||||
|
||||
#### 프론트엔드 구현 사항
|
||||
|
||||
1. **메뉴 갱신 유틸리티 함수** (`src/lib/utils/menuRefresh.ts`)
|
||||
2. **폴링 훅** (`src/hooks/useMenuPolling.ts`)
|
||||
3. **AuthenticatedLayout에 훅 적용**
|
||||
|
||||
#### 백엔드 요청 사항
|
||||
|
||||
| 항목 | 설명 |
|
||||
|------|------|
|
||||
| **엔드포인트** | `GET /api/v1/menus` |
|
||||
| **인증** | Bearer 토큰 필요 |
|
||||
| **응답** | 현재 사용자의 메뉴 목록 (로그인 응답의 menus와 동일 구조) |
|
||||
| **선택사항** | `menu_version` 또는 `menu_hash` 필드 추가 (변경 감지 최적화용) |
|
||||
|
||||
---
|
||||
|
||||
### 2단계: SSE 고도화 (향후 계획)
|
||||
|
||||
**방식**: 서버에서 메뉴 변경 시 SSE로 클라이언트에 푸시
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 백엔드 (Laravel) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 1. 관리자가 메뉴 추가 → DB 저장 │
|
||||
│ 2. MenuUpdatedEvent 발생 │
|
||||
│ 3. 해당 테넌트의 SSE 채널로 푸시 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓ SSE
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 프론트엔드 (Next.js) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 1. EventSource로 SSE 연결 유지 │
|
||||
│ 2. 'menu-updated' 이벤트 수신 │
|
||||
│ 3. refreshMenus() 호출 → UI 즉시 갱신 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**장점**:
|
||||
- 실시간 갱신 (지연 없음)
|
||||
- 효율적 (변경 시에만 통신)
|
||||
|
||||
**단점**:
|
||||
- 백엔드 SSE 인프라 구축 필요
|
||||
- 동시 접속자 관리 필요
|
||||
- 멀티테넌트 채널 분리 필요
|
||||
|
||||
#### 백엔드 요구사항 (SSE)
|
||||
|
||||
| 항목 | 설명 |
|
||||
|------|------|
|
||||
| **SSE 엔드포인트** | `GET /api/v1/sse/menu-updates` |
|
||||
| **인증** | Bearer 토큰 또는 쿼리 파라미터 |
|
||||
| **이벤트 타입** | `menu-updated` |
|
||||
| **채널 분리** | 테넌트별로 분리 필요 |
|
||||
| **구현 옵션** | Laravel Broadcasting + Redis, 직접 구현 등 |
|
||||
|
||||
---
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
### 1단계: 폴링 방식
|
||||
|
||||
#### 프론트엔드 ✅ 구현 완료 (2025-12-29)
|
||||
- [x] `src/lib/utils/menuRefresh.ts` 생성
|
||||
- [x] `refreshMenus()` 함수 구현
|
||||
- [x] `forceRefreshMenus()` 강제 갱신 함수
|
||||
- [x] localStorage + Zustand 동시 업데이트
|
||||
- [x] 해시 기반 변경 감지
|
||||
- [x] `src/hooks/useMenuPolling.ts` 생성
|
||||
- [x] 30초 간격 폴링 로직
|
||||
- [x] 탭 가시성 변경 시 자동 중지/재개
|
||||
- [x] pause/resume 기능
|
||||
- [x] 컴포넌트 언마운트 시 정리
|
||||
- [x] `src/app/api/menus/route.ts` 생성 (Next.js 프록시)
|
||||
- [x] 백엔드 메뉴 API 프록시
|
||||
- [x] HttpOnly 쿠키 토큰 처리
|
||||
- [x] `{ data: [...] }` 응답 구조 처리
|
||||
- [x] `AuthenticatedLayout.tsx`에 훅 적용
|
||||
- [ ] 테스트: 관리자 메뉴 추가 → 30초 내 사용자 메뉴 갱신 확인
|
||||
|
||||
#### 백엔드 (이미 존재!)
|
||||
- [x] `GET /api/v1/menus` API 존재 확인 ✅
|
||||
- [x] `MenuController::index` → `MenuService::index` (사용자 권한 기반 필터링)
|
||||
- [x] 응답 구조: `{ data: [...] }` (ApiResponse::handle 표준)
|
||||
|
||||
### 2단계: SSE 고도화 (향후)
|
||||
|
||||
- [ ] 백엔드 SSE 인프라 구축
|
||||
- [ ] 프론트엔드 EventSource 훅 구현
|
||||
- [ ] 폴링 → SSE 전환
|
||||
- [ ] 폴백: SSE 연결 실패 시 폴링으로 대체
|
||||
|
||||
---
|
||||
|
||||
## 코드 스니펫
|
||||
|
||||
### refreshMenus 함수
|
||||
|
||||
```typescript
|
||||
// src/lib/utils/menuRefresh.ts
|
||||
import { transformApiMenusToMenuItems, deserializeMenuItems } from './menuTransform';
|
||||
import { useMenuStore } from '@/store/menuStore';
|
||||
|
||||
export async function refreshMenus(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch('/api/menus');
|
||||
if (!response.ok) return false;
|
||||
|
||||
const { menus } = await response.json();
|
||||
const transformedMenus = transformApiMenusToMenuItems(menus);
|
||||
|
||||
// 1. localStorage 업데이트 (새로고침 대응)
|
||||
const userData = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
userData.menu = transformedMenus;
|
||||
localStorage.setItem('user', JSON.stringify(userData));
|
||||
|
||||
// 2. Zustand 스토어 업데이트 (UI 즉시 반영)
|
||||
const { setMenuItems } = useMenuStore.getState();
|
||||
setMenuItems(deserializeMenuItems(transformedMenus));
|
||||
|
||||
console.log('[Menu] 메뉴 갱신 완료');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[Menu] 메뉴 갱신 실패:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### useMenuPolling 훅
|
||||
|
||||
```typescript
|
||||
// src/hooks/useMenuPolling.ts
|
||||
// 주요 기능: 30초 폴링, 탭 가시성 처리, 세션 만료 감지(3회 연속 401), 토큰 갱신 쿠키 감지
|
||||
|
||||
export function useMenuPolling(options: UseMenuPollingOptions = {}): UseMenuPollingReturn {
|
||||
// ⚠️ 콜백 안정화 패턴 (2026-02-03 버그 수정)
|
||||
// 부모 컴포넌트에서 인라인 콜백을 전달하면 매 렌더마다 새 참조가 생성되어
|
||||
// executeRefresh → useEffect 의존성이 변경 → setInterval이 매 렌더마다 리셋되는 버그 발생.
|
||||
// 해결: 콜백을 ref로 저장하여 executeRefresh 의존성에서 제거.
|
||||
const onMenuUpdatedRef = useRef(onMenuUpdated);
|
||||
const onErrorRef = useRef(onError);
|
||||
const onSessionExpiredRef = useRef(onSessionExpired);
|
||||
|
||||
useEffect(() => {
|
||||
onMenuUpdatedRef.current = onMenuUpdated;
|
||||
onErrorRef.current = onError;
|
||||
onSessionExpiredRef.current = onSessionExpired;
|
||||
});
|
||||
|
||||
// executeRefresh 의존성: [stopPolling] 만 — 안정적
|
||||
const executeRefresh = useCallback(async () => {
|
||||
// ref를 통해 최신 콜백 호출
|
||||
onMenuUpdatedRef.current?.();
|
||||
onSessionExpiredRef.current?.();
|
||||
onErrorRef.current?.(result.error);
|
||||
}, [stopPolling]);
|
||||
}
|
||||
```
|
||||
|
||||
### Next.js API 프록시
|
||||
|
||||
```typescript
|
||||
// src/app/api/menus/route.ts
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const token = request.cookies.get('access_token')?.value;
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/menus`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 참고 사항
|
||||
|
||||
### 메뉴 데이터 저장 위치
|
||||
|
||||
| 저장소 | 키 | 용도 |
|
||||
|--------|-----|------|
|
||||
| localStorage | `user.menu` | 새로고침 시 복구용 |
|
||||
| Zustand | `menuStore.menuItems` | UI 렌더링용 |
|
||||
|
||||
### 갱신 시 동기화 필수
|
||||
|
||||
```typescript
|
||||
// 반드시 둘 다 업데이트!
|
||||
localStorage.user.menu = newMenus; // 새로고침 대응
|
||||
menuStore.setMenuItems(newMenus); // UI 즉시 반영
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 작성 정보
|
||||
|
||||
- **작성일**: 2025-12-29
|
||||
- **최종 수정**: 2026-02-03
|
||||
- **상태**: ✅ 1단계 구현 완료 + 폴링 버그 수정
|
||||
- **담당**: 프론트엔드 팀
|
||||
- **백엔드**: `GET /api/v1/menus` API 이미 존재 ✅
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
|
||||
### 2026-02-03: 폴링 인터벌 리셋 버그 수정
|
||||
|
||||
**문제**: 메뉴 폴링이 실제로 실행되지 않아 백엔드에서 메뉴를 추가해도 재로그인 전까지 반영되지 않음.
|
||||
|
||||
**원인**: `useMenuPolling` 훅의 `executeRefresh` 콜백이 매 렌더마다 새 참조를 생성하여 `setInterval`이 리셋됨.
|
||||
|
||||
```
|
||||
AuthenticatedLayout에서 인라인 콜백 전달:
|
||||
onMenuUpdated: () => { ... } ← 매 렌더마다 새 함수
|
||||
onSessionExpired: () => { ... } ← 매 렌더마다 새 함수
|
||||
↓
|
||||
executeRefresh deps: [onMenuUpdated, onError, onSessionExpired, stopPolling]
|
||||
↓ 매 렌더마다 변경
|
||||
useEffect deps: [executeRefresh] → clearInterval → setInterval 재설정
|
||||
↓
|
||||
알림 폴링이 30초마다 state 업데이트 → 리렌더 → 메뉴 인터벌 리셋
|
||||
↓
|
||||
메뉴 폴링이 30초에 도달하지 못하고 영원히 미실행
|
||||
```
|
||||
|
||||
**수정**: 콜백을 `useRef`로 안정화하여 `executeRefresh` 의존성에서 제거.
|
||||
|
||||
```
|
||||
수정 전: executeRefresh deps = [onMenuUpdated, onError, onSessionExpired, stopPolling]
|
||||
수정 후: executeRefresh deps = [stopPolling] ← 안정적, 인터벌 리셋 없음
|
||||
```
|
||||
|
||||
**수정 파일**: `src/hooks/useMenuPolling.ts`
|
||||
@@ -0,0 +1,96 @@
|
||||
# 레이아웃 구조 변경 계획
|
||||
|
||||
> **상태**: 📋 대기 (기능 검수 완료 후 진행)
|
||||
> **작성일**: 2026-01-16
|
||||
> **적용 대상**: IntegratedListTemplateV2.tsx (55개 페이지 일괄 적용)
|
||||
|
||||
---
|
||||
|
||||
## 현재 구조
|
||||
|
||||
```
|
||||
1. 타이틀
|
||||
2. 달력 / 버튼들 (등록 버튼 여기)
|
||||
3. 통계 카드
|
||||
4. 검색창 (Card로 감싸짐)
|
||||
5. 테이블 Card
|
||||
└─ 탭 버튼들 / 필터 / 삭제 버튼
|
||||
└─ 테이블
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 변경 후 구조
|
||||
|
||||
```
|
||||
1. 타이틀
|
||||
2. 달력 / 달력버튼 / 검색창 (한 줄)
|
||||
3. 카드섹션 (한 줄, 줄넘김 없음)
|
||||
4. [탭 버튼들] ─────────────── [등록] [CSV] 버튼들 ← Card 밖
|
||||
5. 테이블 Card
|
||||
├─ 총 N건 / 선택건 / 필터
|
||||
└─ 테이블
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 시각화
|
||||
|
||||
```
|
||||
┌─ 페이지 ─────────────────────────────────────────────────┐
|
||||
│ 휴가관리 │
|
||||
│ 직원들의 휴가 현황을 관리합니다 │
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ [📅 2025-12-01] ~ [📅 2025-12-31] [당월][전월] [🔍검색] │
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ [승인대기 1명] [연차 4명] [경조사 0명] [사용률 4.3%] │ ← 카드 (줄넘김X)
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ [사용현황 4] [부여현황 2] [신청현황 3] [등록] [CSV] │ ← Card 밖
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ ┌─ 테이블 Card ────────────────────────────────────────┐ │
|
||||
│ │ 총 55건 | 3개 선택됨 [필터1] [필터2] │ │
|
||||
│ ├──────────────────────────────────────────────────────┤ │
|
||||
│ │ □ | 번호 | 부서 | 이름 | ... │ │
|
||||
│ │ □ | 1 | 개발 | 홍길동 | ... │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 주요 변경점
|
||||
|
||||
| 항목 | 현재 | 변경 후 |
|
||||
|------|------|---------|
|
||||
| 검색창 | Card로 감싸짐, 별도 영역 | 달력 옆 한 줄에 배치 |
|
||||
| 카드섹션 | flex-wrap (줄넘김) | flex-nowrap + overflow-x-auto |
|
||||
| 탭 버튼 | 테이블 Card 내부 | 테이블 Card 위 (밖) |
|
||||
| 등록/액션 버튼 | 헤더 영역 | 탭 버튼 오른쪽 |
|
||||
| 총 N건/선택건 | 탭과 같은 줄 | 테이블 Card 내부 첫 줄 |
|
||||
| 필터 | 탭과 같은 줄 | 테이블 Card 내부 첫 줄 |
|
||||
|
||||
---
|
||||
|
||||
## 수정 대상 파일
|
||||
|
||||
1. **IntegratedListTemplateV2.tsx** - 전체 레이아웃 구조 변경
|
||||
2. **UniversalListPage/index.tsx** - prop 전달 방식 조정 (필요시)
|
||||
|
||||
---
|
||||
|
||||
## 체크리스트
|
||||
|
||||
- [ ] 검색창 위치 이동 (달력 옆)
|
||||
- [ ] 카드섹션 줄넘김 방지 (flex-nowrap)
|
||||
- [ ] 탭 버튼 테이블 Card 밖으로 이동
|
||||
- [ ] 등록/액션 버튼 탭 옆으로 이동
|
||||
- [ ] 총 N건/선택건/필터 테이블 Card 내부로 이동
|
||||
- [ ] PC/모바일 반응형 확인
|
||||
- [ ] 55개 페이지 일괄 테스트
|
||||
|
||||
---
|
||||
|
||||
## 진행 조건
|
||||
|
||||
✅ **기능 검수 완료 후 진행**
|
||||
- 현재 화면과 비교 검수가 필요하므로 레이아웃 변경은 기능 검수 이후에 진행
|
||||
@@ -0,0 +1,546 @@
|
||||
# UI 컴포넌트 공통화/추상화 계획
|
||||
|
||||
> **작성일**: 2026-01-22
|
||||
> **상태**: 🟢 진행 중
|
||||
> **범위**: 공통 UI 컴포넌트 추상화 및 스켈레톤 시스템 구축
|
||||
|
||||
---
|
||||
|
||||
## 결정 사항 (2026-01-22)
|
||||
|
||||
| 항목 | 결정 |
|
||||
|------|------|
|
||||
| 스켈레톤 전환 범위 | **Option A: 전체 스켈레톤 전환** |
|
||||
| 구현 우선순위 | **Phase 1 먼저** (ConfirmDialog → StatusBadge → EmptyState) |
|
||||
| 확장 전략 | **옵션 기반 확장** - 새 패턴 발견 시 props 옵션으로 추가 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 현황 분석 요약
|
||||
|
||||
### 반복 패턴 현황
|
||||
|
||||
| 패턴 | 파일 수 | 발생 횟수 | 복잡도 | 우선순위 |
|
||||
|------|---------|----------|--------|----------|
|
||||
| 확인 다이얼로그 (삭제/저장) | 67개 | 170회 | 낮음 | 🔴 높음 |
|
||||
| 상태 스타일 매핑 | 80개 | 다수 | 낮음 | 🔴 높음 |
|
||||
| 날짜 범위 필터 | 55개 | 146회 | 중간 | 🟡 중간 |
|
||||
| 빈 상태 UI | 70개 | 86회 | 낮음 | 🟡 중간 |
|
||||
| 로딩 스피너/버튼 | 59개 | 120회 | 중간 | 🟡 중간 |
|
||||
| 스켈레톤 UI | 4개 | 92회 | 높음 | 🔴 높음 |
|
||||
|
||||
### 현재 스켈레톤 현황
|
||||
|
||||
**기존 구현:**
|
||||
- `src/components/ui/skeleton.tsx` - 기본 스켈레톤 (단순 animate-pulse div)
|
||||
- `IntegratedDetailTemplate/components/skeletons/` - 상세 페이지용 3종
|
||||
- `DetailFieldSkeleton.tsx`
|
||||
- `DetailSectionSkeleton.tsx`
|
||||
- `DetailGridSkeleton.tsx`
|
||||
- `loading.tsx` - 4개 파일만 존재 (대부분 PageLoadingSpinner 사용)
|
||||
|
||||
**문제점:**
|
||||
1. 대부분 페이지에서 로딩 스피너 사용 (스켈레톤 미적용)
|
||||
2. 리스트 페이지용 스켈레톤 없음
|
||||
3. 카드/대시보드용 스켈레톤 없음
|
||||
4. 페이지별 loading.tsx 부재 (4개만 존재)
|
||||
|
||||
---
|
||||
|
||||
## 2. 공통화 대상 상세
|
||||
|
||||
### Phase 1: 핵심 공통 컴포넌트 (1주차)
|
||||
|
||||
#### 1-1. ConfirmDialog 컴포넌트
|
||||
|
||||
**현재 (반복 코드):**
|
||||
```tsx
|
||||
// 67개 파일에서 거의 동일하게 반복
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>삭제 확인</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
정말 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isLoading}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
```
|
||||
|
||||
**개선안:**
|
||||
```tsx
|
||||
// src/components/ui/confirm-dialog.tsx
|
||||
interface ConfirmDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
title: string;
|
||||
description: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
variant?: 'default' | 'destructive' | 'warning';
|
||||
loading?: boolean;
|
||||
onConfirm: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
// 사용 예시
|
||||
<ConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
title="삭제 확인"
|
||||
description="정말 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다."
|
||||
confirmText="삭제"
|
||||
variant="destructive"
|
||||
loading={isLoading}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
```
|
||||
|
||||
**효과:**
|
||||
- 코드량: ~30줄 → ~10줄 (70% 감소)
|
||||
- 일관된 UX 보장
|
||||
- 로딩 상태 자동 처리
|
||||
|
||||
---
|
||||
|
||||
#### 1-2. StatusBadge 컴포넌트 + createStatusConfig 유틸
|
||||
|
||||
**현재 (반복 코드):**
|
||||
```tsx
|
||||
// 80개 파일에서 각각 정의
|
||||
// estimates/types.ts
|
||||
export const STATUS_STYLES: Record<string, string> = {
|
||||
pending: 'bg-yellow-100 text-yellow-800',
|
||||
inProgress: 'bg-blue-100 text-blue-800',
|
||||
completed: 'bg-green-100 text-green-800',
|
||||
};
|
||||
export const STATUS_LABELS: Record<string, string> = {
|
||||
pending: '대기',
|
||||
inProgress: '진행중',
|
||||
completed: '완료',
|
||||
};
|
||||
|
||||
// site-management/types.ts (거의 동일)
|
||||
export const SITE_STATUS_STYLES: Record<string, string> = { ... };
|
||||
export const SITE_STATUS_LABELS: Record<string, string> = { ... };
|
||||
```
|
||||
|
||||
**개선안:**
|
||||
```tsx
|
||||
// src/lib/utils/status-config.ts
|
||||
export type StatusVariant = 'default' | 'success' | 'warning' | 'error' | 'info';
|
||||
|
||||
export interface StatusConfig<T extends string> {
|
||||
value: T;
|
||||
label: string;
|
||||
variant: StatusVariant;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export function createStatusConfig<T extends string>(
|
||||
configs: StatusConfig<T>[]
|
||||
): {
|
||||
options: { value: T; label: string }[];
|
||||
getLabel: (status: T) => string;
|
||||
getVariant: (status: T) => StatusVariant;
|
||||
isValid: (status: string) => status is T;
|
||||
}
|
||||
|
||||
// src/components/ui/status-badge.tsx
|
||||
interface StatusBadgeProps<T extends string> {
|
||||
status: T;
|
||||
config: ReturnType<typeof createStatusConfig<T>>;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
// 사용 예시
|
||||
// estimates/types.ts
|
||||
export const estimateStatusConfig = createStatusConfig([
|
||||
{ value: 'pending', label: '대기', variant: 'warning' },
|
||||
{ value: 'inProgress', label: '진행중', variant: 'info' },
|
||||
{ value: 'completed', label: '완료', variant: 'success' },
|
||||
]);
|
||||
|
||||
// 컴포넌트에서
|
||||
<StatusBadge status={data.status} config={estimateStatusConfig} />
|
||||
```
|
||||
|
||||
**효과:**
|
||||
- 타입 안전성 강화
|
||||
- 일관된 색상 체계
|
||||
- options 자동 생성 (Select용)
|
||||
|
||||
---
|
||||
|
||||
#### 1-3. EmptyState 컴포넌트
|
||||
|
||||
**현재 (반복 코드):**
|
||||
```tsx
|
||||
// 70개 파일에서 다양한 형태로 반복
|
||||
{data.length === 0 && (
|
||||
<div className="text-center py-10 text-muted-foreground">
|
||||
데이터가 없습니다
|
||||
</div>
|
||||
)}
|
||||
|
||||
// 또는
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="text-center py-8">
|
||||
등록된 항목이 없습니다
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
```
|
||||
|
||||
**개선안:**
|
||||
```tsx
|
||||
// src/components/ui/empty-state.tsx
|
||||
interface EmptyStateProps {
|
||||
icon?: ReactNode;
|
||||
title?: string;
|
||||
description?: string;
|
||||
action?: ReactNode;
|
||||
variant?: 'default' | 'table' | 'card' | 'minimal';
|
||||
}
|
||||
|
||||
// 사용 예시
|
||||
<EmptyState
|
||||
icon={<FileX className="w-12 h-12" />}
|
||||
title="데이터가 없습니다"
|
||||
description="새로운 항목을 등록하거나 검색 조건을 변경해보세요."
|
||||
action={<Button onClick={onCreate}>등록하기</Button>}
|
||||
/>
|
||||
|
||||
// 테이블 내 사용
|
||||
<EmptyState variant="table" colSpan={10} title="검색 결과가 없습니다" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: 스켈레톤 시스템 구축 (2주차)
|
||||
|
||||
#### 2-1. 스켈레톤 컴포넌트 확장
|
||||
|
||||
**현재 문제:**
|
||||
- 기본 Skeleton만 존재 (단순 div)
|
||||
- 페이지 유형별 스켈레톤 부재
|
||||
- 대부분 PageLoadingSpinner 사용 (스켈레톤 미적용)
|
||||
|
||||
**추가할 스켈레톤:**
|
||||
|
||||
```tsx
|
||||
// src/components/ui/skeletons/
|
||||
├── index.ts // 통합 export
|
||||
├── ListPageSkeleton.tsx // 리스트 페이지용
|
||||
├── DetailPageSkeleton.tsx // 상세 페이지용 (기존 확장)
|
||||
├── CardGridSkeleton.tsx // 카드 그리드용
|
||||
├── DashboardSkeleton.tsx // 대시보드용
|
||||
├── TableSkeleton.tsx // 테이블용
|
||||
├── FormSkeleton.tsx // 폼용
|
||||
└── ChartSkeleton.tsx // 차트용
|
||||
```
|
||||
|
||||
**1. ListPageSkeleton (리스트 페이지용)**
|
||||
```tsx
|
||||
interface ListPageSkeletonProps {
|
||||
hasFilters?: boolean;
|
||||
filterCount?: number;
|
||||
hasDateRange?: boolean;
|
||||
rowCount?: number;
|
||||
columnCount?: number;
|
||||
hasActions?: boolean;
|
||||
hasPagination?: boolean;
|
||||
}
|
||||
|
||||
// 사용 예시
|
||||
export default function EstimateListLoading() {
|
||||
return (
|
||||
<ListPageSkeleton
|
||||
hasFilters
|
||||
filterCount={4}
|
||||
hasDateRange
|
||||
rowCount={10}
|
||||
columnCount={8}
|
||||
hasActions
|
||||
hasPagination
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**2. CardGridSkeleton (카드 그리드용)**
|
||||
```tsx
|
||||
interface CardGridSkeletonProps {
|
||||
cardCount?: number;
|
||||
cols?: 1 | 2 | 3 | 4;
|
||||
cardHeight?: 'sm' | 'md' | 'lg';
|
||||
hasImage?: boolean;
|
||||
hasFooter?: boolean;
|
||||
}
|
||||
|
||||
// 대시보드 카드, 칸반 보드 등에 사용
|
||||
<CardGridSkeleton cardCount={6} cols={3} cardHeight="md" />
|
||||
```
|
||||
|
||||
**3. TableSkeleton (테이블용)**
|
||||
```tsx
|
||||
interface TableSkeletonProps {
|
||||
rowCount?: number;
|
||||
columnCount?: number;
|
||||
hasCheckbox?: boolean;
|
||||
hasActions?: boolean;
|
||||
columnWidths?: string[]; // ['w-12', 'w-32', 'flex-1', ...]
|
||||
}
|
||||
|
||||
<TableSkeleton rowCount={10} columnCount={8} hasCheckbox hasActions />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2-2. loading.tsx 파일 생성 전략
|
||||
|
||||
**현재:** 4개 파일만 존재
|
||||
**목표:** 주요 페이지 경로에 맞춤형 loading.tsx 생성
|
||||
|
||||
**생성 대상 (우선순위):**
|
||||
|
||||
| 경로 | 스켈레톤 타입 | 우선순위 |
|
||||
|------|-------------|----------|
|
||||
| `/construction/project/bidding/estimates` | ListPageSkeleton | 🔴 |
|
||||
| `/construction/project/bidding` | ListPageSkeleton | 🔴 |
|
||||
| `/construction/project/contract` | ListPageSkeleton | 🔴 |
|
||||
| `/construction/order/*` | ListPageSkeleton | 🔴 |
|
||||
| `/accounting/*` | ListPageSkeleton | 🟡 |
|
||||
| `/hr/*` | ListPageSkeleton | 🟡 |
|
||||
| `/settings/*` | ListPageSkeleton | 🟢 |
|
||||
| `상세 페이지` | DetailPageSkeleton | 🟡 |
|
||||
| `대시보드` | DashboardSkeleton | 🟡 |
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: 날짜 범위 필터 + 로딩 버튼 (3주차)
|
||||
|
||||
#### 3-1. DateRangeFilter 컴포넌트
|
||||
|
||||
**현재 (반복 코드):**
|
||||
```tsx
|
||||
// 55개 파일에서 반복
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Input type="date" value={startDate} onChange={...} />
|
||||
<span>~</span>
|
||||
<Input type="date" value={endDate} onChange={...} />
|
||||
</div>
|
||||
```
|
||||
|
||||
**개선안:**
|
||||
```tsx
|
||||
// src/components/ui/date-range-filter.tsx
|
||||
interface DateRangeFilterProps {
|
||||
value: { start: string; end: string };
|
||||
onChange: (range: { start: string; end: string }) => void;
|
||||
presets?: ('today' | 'week' | 'month' | 'quarter' | 'year')[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// 사용 예시
|
||||
<DateRangeFilter
|
||||
value={{ start: startDate, end: endDate }}
|
||||
onChange={({ start, end }) => {
|
||||
setStartDate(start);
|
||||
setEndDate(end);
|
||||
}}
|
||||
presets={['today', 'week', 'month']}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 3-2. LoadingButton 컴포넌트
|
||||
|
||||
**현재 (반복 코드):**
|
||||
```tsx
|
||||
// 59개 파일에서 반복
|
||||
<Button disabled={isLoading}>
|
||||
{isLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||
저장
|
||||
</Button>
|
||||
```
|
||||
|
||||
**개선안:**
|
||||
```tsx
|
||||
// src/components/ui/loading-button.tsx
|
||||
interface LoadingButtonProps extends ButtonProps {
|
||||
loading?: boolean;
|
||||
loadingText?: string;
|
||||
spinnerPosition?: 'left' | 'right';
|
||||
}
|
||||
|
||||
// 사용 예시
|
||||
<LoadingButton loading={isLoading} loadingText="저장 중...">
|
||||
저장
|
||||
</LoadingButton>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 로딩 스피너 vs 스켈레톤 전략
|
||||
|
||||
### 논의 사항
|
||||
|
||||
**Option A: 전체 스켈레톤 전환**
|
||||
- 장점: 더 나은 UX, 레이아웃 시프트 방지
|
||||
- 단점: 구현 비용 높음, 페이지별 커스텀 필요
|
||||
|
||||
**Option B: 하이브리드 (권장)**
|
||||
- 페이지 로딩: 스켈레톤 (loading.tsx)
|
||||
- 버튼/액션 로딩: 스피너 유지 (LoadingButton)
|
||||
- 데이터 갱신: 스피너 유지
|
||||
|
||||
**Option C: 현행 유지**
|
||||
- 대부분 스피너 유지
|
||||
- 특정 페이지만 스켈레톤
|
||||
|
||||
### 권장안: Option B (하이브리드)
|
||||
|
||||
| 상황 | 로딩 UI | 이유 |
|
||||
|------|---------|------|
|
||||
| 페이지 초기 로딩 | 스켈레톤 | 레이아웃 힌트 제공 |
|
||||
| 페이지 전환 | 스켈레톤 | Next.js loading.tsx 활용 |
|
||||
| 버튼 클릭 (저장/삭제) | 스피너 | 짧은 작업, 버튼 내 피드백 |
|
||||
| 데이터 갱신 (필터 변경) | 스피너 or 스켈레톤 | 상황에 따라 |
|
||||
| 무한 스크롤 | 스켈레톤 | 추가 컨텐츠 힌트 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 구현 로드맵
|
||||
|
||||
### Week 1: 핵심 컴포넌트
|
||||
- [x] ConfirmDialog 컴포넌트 생성 ✅ (2026-01-22)
|
||||
- `src/components/ui/confirm-dialog.tsx`
|
||||
- variants: default, destructive, warning, success
|
||||
- presets: DeleteConfirmDialog, SaveConfirmDialog, CancelConfirmDialog
|
||||
- 내부/외부 로딩 상태 자동 관리
|
||||
- [x] StatusBadge + createStatusConfig 유틸 생성 ✅ (2026-01-22)
|
||||
- `src/lib/utils/status-config.ts`
|
||||
- `src/components/ui/status-badge.tsx`
|
||||
- 프리셋: default, success, warning, destructive, info, muted, orange, purple
|
||||
- 모드: badge (배경+텍스트), text (텍스트만)
|
||||
- OPTIONS, LABELS, STYLES 자동 생성
|
||||
- [x] EmptyState 컴포넌트 생성 ✅ (2026-01-22)
|
||||
- `src/components/ui/empty-state.tsx`
|
||||
- variants: default, compact, large
|
||||
- presets: noData, noResults, noItems, error
|
||||
- TableEmptyState 추가 (테이블용)
|
||||
- [x] 기존 코드 마이그레이션 (10개 파일 시범) ✅ (2026-01-22)
|
||||
- PricingDetailClient.tsx - 삭제 확인
|
||||
- ItemManagementClient.tsx - 단일/일괄 삭제
|
||||
- LaborDetailClient.tsx - 삭제 확인
|
||||
- ConstructionDetailClient.tsx - 완료 확인 (warning)
|
||||
- QuoteManagementClient.tsx - 단일/일괄 삭제
|
||||
- OrderDialogs.tsx - 저장/삭제/카테고리삭제
|
||||
- DepartmentManagement/index.tsx - 삭제 확인
|
||||
- VacationManagement/index.tsx - 승인/거절 확인
|
||||
- AccountDetail.tsx - 삭제 확인
|
||||
- ProcessListClient.tsx - 삭제 확인
|
||||
|
||||
### Week 2: 스켈레톤 시스템
|
||||
- [ ] ListPageSkeleton 컴포넌트 생성
|
||||
- [ ] TableSkeleton 컴포넌트 생성
|
||||
- [ ] CardGridSkeleton 컴포넌트 생성
|
||||
- [ ] 주요 경로 loading.tsx 생성 (construction/*)
|
||||
|
||||
### Week 3: 필터 + 버튼 + 마이그레이션
|
||||
- [ ] DateRangeFilter 컴포넌트 생성
|
||||
- [ ] LoadingButton 컴포넌트 생성
|
||||
- [ ] 전체 코드 마이그레이션
|
||||
|
||||
### Week 4: 마무리 + QA
|
||||
- [ ] 남은 마이그레이션
|
||||
- [ ] 문서화
|
||||
- [ ] 성능 테스트
|
||||
|
||||
---
|
||||
|
||||
## 5. 예상 효과
|
||||
|
||||
### 코드량 감소
|
||||
| 컴포넌트 | Before | After | 감소율 |
|
||||
|---------|--------|-------|--------|
|
||||
| ConfirmDialog | ~30줄 | ~10줄 | 67% |
|
||||
| StatusBadge | ~20줄 | ~5줄 | 75% |
|
||||
| EmptyState | ~10줄 | ~3줄 | 70% |
|
||||
| DateRangeFilter | ~15줄 | ~5줄 | 67% |
|
||||
|
||||
### 일관성 향상
|
||||
- 동일한 UX 패턴 적용
|
||||
- 디자인 시스템 강화
|
||||
- 유지보수 용이성 증가
|
||||
|
||||
### 성능 개선
|
||||
- 스켈레톤으로 인지 성능 향상
|
||||
- 레이아웃 시프트 감소
|
||||
- 사용자 이탈률 감소
|
||||
|
||||
---
|
||||
|
||||
## 6. 결정 필요 사항
|
||||
|
||||
### Q1: 스켈레톤 전환 범위
|
||||
- [ ] Option A: 전체 스켈레톤 전환
|
||||
- [ ] Option B: 하이브리드 (권장)
|
||||
- [ ] Option C: 현행 유지
|
||||
|
||||
### Q2: 구현 우선순위
|
||||
- [ ] Phase 1 먼저 (ConfirmDialog, StatusBadge, EmptyState)
|
||||
- [ ] Phase 2 먼저 (스켈레톤 시스템)
|
||||
- [ ] 동시 진행
|
||||
|
||||
### Q3: 마이그레이션 범위
|
||||
- [ ] 전체 파일 한번에
|
||||
- [ ] 점진적 (신규/수정 파일만)
|
||||
- [ ] 도메인별 순차 (construction → accounting → hr)
|
||||
|
||||
---
|
||||
|
||||
## 7. 파일 구조 (최종)
|
||||
|
||||
```
|
||||
src/components/ui/
|
||||
├── confirm-dialog.tsx # Phase 1
|
||||
├── status-badge.tsx # Phase 1
|
||||
├── empty-state.tsx # Phase 1
|
||||
├── date-range-filter.tsx # Phase 3
|
||||
├── loading-button.tsx # Phase 3
|
||||
├── skeleton.tsx # 기존
|
||||
└── skeletons/ # Phase 2
|
||||
├── index.ts
|
||||
├── ListPageSkeleton.tsx
|
||||
├── DetailPageSkeleton.tsx
|
||||
├── CardGridSkeleton.tsx
|
||||
├── DashboardSkeleton.tsx
|
||||
├── TableSkeleton.tsx
|
||||
├── FormSkeleton.tsx
|
||||
└── ChartSkeleton.tsx
|
||||
|
||||
src/lib/utils/
|
||||
└── status-config.ts # Phase 1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**다음 단계**: 위 결정 사항에 대한 의견 확정 후 구현 시작
|
||||
@@ -0,0 +1,666 @@
|
||||
# 멀티테넌시 공통화 및 최적화 로드맵
|
||||
|
||||
**작성일**: 2026-02-06
|
||||
**목적**: 전체 프로젝트 멀티테넌시 준비 상태 점검 및 공통화/최적화 계획 수립
|
||||
**이전 문서**: `[REF-2025-11-19] multi-tenancy-implementation.md` (Phase 1-2 완료)
|
||||
|
||||
---
|
||||
|
||||
## 현재 상태 요약 (2026-02-06 기준)
|
||||
|
||||
### 완료된 항목 (이전 로드맵 Phase 1-2)
|
||||
|
||||
| 항목 | 상태 | 파일 |
|
||||
|------|------|------|
|
||||
| User 타입에 Tenant 객체 포함 | ✅ 완료 | `src/contexts/AuthContext.tsx` |
|
||||
| Tenant 인터페이스 정의 (id, company_name 등) | ✅ 완료 | `src/contexts/AuthContext.tsx` |
|
||||
| TenantAwareCache 유틸리티 | ✅ 완료 | `src/lib/cache/TenantAwareCache.ts` |
|
||||
| 테넌트 전환 감지 + 캐시 클리어 | ✅ 완료 | `src/contexts/AuthContext.tsx` |
|
||||
| masterDataStore 테넌트 스코프 캐시 키 | ✅ 완료 | `src/stores/masterDataStore.ts` |
|
||||
| sessionStorage/localStorage 테넌트 격리 | ✅ 완료 | `mes-{tenantId}-{key}` 패턴 |
|
||||
|
||||
### 미완료 / 개선 필요 항목
|
||||
|
||||
| 영역 | 우선순위 | 현재 상태 |
|
||||
|------|----------|-----------|
|
||||
| API 프록시에 테넌트 컨텍스트 전달 | 🔴 | X-Tenant-ID 헤더 없음 |
|
||||
| Server Actions 테넌트 인식 | 🔴 | 70+ actions.ts에 테넌트 미포함 |
|
||||
| 포매터/유틸리티 다국어/다통화 | 🔴 | 한국어 하드코딩 |
|
||||
| 브랜딩 동적화 (로고, 앱이름) | 🟡 | "SAM", sam-logo.png 하드코딩 |
|
||||
| 상수/공휴일 외부화 | 🟡 | 한국 공휴일 하드코딩 |
|
||||
| localStorage 직접 사용 잔재 | 🟡 | TenantAwareCache 미사용 곳 존재 |
|
||||
| tenantId 타입 불일치 | 🟡 | string vs number 혼재 |
|
||||
| 테넌트 라우팅 | 🟢 | 현재 없음 (필요 시 추가) |
|
||||
| TenantContext Provider | 🟢 | 테넌트 설정 전용 Context 없음 |
|
||||
|
||||
---
|
||||
|
||||
## 작업 영역 구분: 프론트 단독 vs 백엔드 협의
|
||||
|
||||
### 선행 확인 사항
|
||||
|
||||
> **핵심 질문**: "백엔드가 이미 JWT 토큰 안의 tenant_id로 데이터를 필터링하고 있는가?"
|
||||
>
|
||||
> - **Yes** → 프론트에서 별도 X-Tenant-ID 안 보내도 됨. Phase 1은 불필요하고 프론트 단독 Phase부터 진행
|
||||
> - **No** → 백엔드도 같이 수정 필요. Phase 1이 최우선
|
||||
|
||||
### 프론트 단독 가능 (백엔드 수정 불필요)
|
||||
|
||||
| Phase | 작업 | 이유 |
|
||||
|-------|------|------|
|
||||
| **3** | 포매터 다국어/다통화 전환 | `formatAmount()`, `formatDate()` 등 프론트 유틸리티 내부 수정. 기본값을 한국어로 유지하면 하위 호환 |
|
||||
| **6** | localStorage 잔재 정리 + tenantId 타입 통일 | 프론트 코드 정리. TenantAwareCache 미사용 곳 마이그레이션, `string` → `number` 통일 |
|
||||
| **8** | 테넌트 라우팅 (필요 시) | Next.js App Router 구조 변경. 순수 프론트 라우팅 |
|
||||
|
||||
> **즉시 착수 가능**: 백엔드 협의 결과를 기다리지 않고 바로 시작할 수 있음
|
||||
|
||||
### 백엔드 협의 필요 (프론트 + 백엔드 동시 수정)
|
||||
|
||||
| Phase | 작업 | 백엔드 필요 이유 |
|
||||
|-------|------|-----------------|
|
||||
| **1** | API 테넌트 컨텍스트 주입 | 프론트에서 `X-Tenant-ID` 헤더를 보내도, **백엔드가 읽고 필터링**해줘야 의미 있음. 안 읽으면 보내봤자 무용지물 |
|
||||
| **2** | Server Actions 마이그레이션 | Phase 1에 종속. 백엔드가 헤더 or URL로 테넌트를 구분 안 하면 프론트만 바꿔도 소용없음 |
|
||||
| **4** | 브랜딩 동적화 | 테넌트별 로고/앱이름을 **어디서 가져오나?** → 백엔드 API 필요 (`GET /api/v1/tenant/config`) |
|
||||
| **5** | 상수/공휴일 외부화 | 공휴일 데이터를 **DB에서 서빙**해야 함 → 백엔드 API 필요 (`GET /api/v1/holidays?year=2026`) |
|
||||
| **7** | TenantConfigService | 테넌트 설정 통합 API 필요 → branding + regional + features를 한 번에 가져오는 엔드포인트 |
|
||||
|
||||
### 추천 진행 순서
|
||||
|
||||
```
|
||||
[즉시 시작 - 프론트 단독]
|
||||
Phase 3 (포매터) + Phase 6 (localStorage/타입) 병렬 진행
|
||||
|
||||
[백엔드 협의 후 시작]
|
||||
Phase 1 (API 헤더) → Phase 2 (Actions)
|
||||
|
||||
[백엔드 API 준비 후 시작]
|
||||
Phase 7 (TenantConfigService) → Phase 4 (브랜딩) + Phase 5 (상수)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: API 레이어 테넌트 컨텍스트 주입 🔴 `백엔드 협의 필요`
|
||||
|
||||
> **목표**: 모든 백엔드 API 호출에 테넌트 식별 정보가 전달되도록 함
|
||||
> **예상**: 3-5일
|
||||
|
||||
### 1-1. 로그인 시 tenant_id 쿠키 추가
|
||||
|
||||
**파일**: `src/app/api/auth/login/route.ts`
|
||||
|
||||
**현재**: access_token, refresh_token 쿠키만 설정
|
||||
**변경**: tenant_id 쿠키 추가 (HttpOnly, API 프록시에서 읽기용)
|
||||
|
||||
```typescript
|
||||
// 로그인 성공 후 추가
|
||||
const tenantCookie = [
|
||||
`tenant_id=${data.tenant.id}`,
|
||||
'HttpOnly',
|
||||
...(isProduction ? ['Secure'] : []),
|
||||
'SameSite=Lax',
|
||||
'Path=/',
|
||||
`Max-Age=${data.expires_in || 7200}`,
|
||||
].join('; ');
|
||||
response.headers.append('Set-Cookie', tenantCookie);
|
||||
```
|
||||
|
||||
### 1-2. API 프록시에 X-Tenant-ID 헤더 추가
|
||||
|
||||
**파일**: `src/app/api/proxy/[...path]/route.ts`
|
||||
|
||||
**현재**:
|
||||
```typescript
|
||||
const headers = {
|
||||
'Accept': 'application/json',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
};
|
||||
```
|
||||
|
||||
**변경**:
|
||||
```typescript
|
||||
const tenantId = request.cookies.get('tenant_id')?.value;
|
||||
const headers = {
|
||||
'Accept': 'application/json',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
...(tenantId && { 'X-Tenant-ID': tenantId }),
|
||||
};
|
||||
```
|
||||
|
||||
### 1-3. serverFetch 래퍼에 테넌트 헤더 추가
|
||||
|
||||
**파일**: `src/lib/api/fetch-wrapper.ts`
|
||||
|
||||
**현재**: Authorization 헤더만 전달
|
||||
**변경**: tenant_id 쿠키 읽어서 X-Tenant-ID 헤더 자동 추가
|
||||
|
||||
```typescript
|
||||
export async function serverFetch(url: string, options?: RequestInit) {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
const tenantId = cookieStore.get('tenant_id')?.value;
|
||||
|
||||
const headers = {
|
||||
...options?.headers,
|
||||
'Authorization': `Bearer ${token}`,
|
||||
...(tenantId && { 'X-Tenant-ID': tenantId }),
|
||||
};
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 1-4. ApiClient 클래스에 테넌트 지원
|
||||
|
||||
**파일**: `src/lib/api/client.ts`
|
||||
|
||||
**변경**: `getAuthHeaders()`에 X-Tenant-ID 포함
|
||||
|
||||
### 체크리스트
|
||||
|
||||
```
|
||||
- [ ] login/route.ts에 tenant_id 쿠키 Set-Cookie 추가
|
||||
- [ ] proxy/[...path]/route.ts에서 tenant_id 쿠키 읽기 + X-Tenant-ID 헤더 전달
|
||||
- [ ] fetch-wrapper.ts serverFetch에 X-Tenant-ID 자동 추가
|
||||
- [ ] client.ts ApiClient에 tenantId 옵션 추가
|
||||
- [ ] authenticated-fetch.ts에도 테넌트 헤더 전파 확인
|
||||
- [ ] 로그아웃 시 tenant_id 쿠키 삭제 확인
|
||||
- [ ] 토큰 갱신 시 tenant_id 쿠키 유지 확인
|
||||
- [ ] 백엔드와 X-Tenant-ID 헤더 수신 방식 협의
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Server Actions 점진적 마이그레이션 🔴 `백엔드 협의 필요`
|
||||
|
||||
> **목표**: 70+ actions.ts에서 테넌트 컨텍스트가 자동 전달되도록 함
|
||||
> **예상**: 1-2주 (Phase 1 완료 후 자동 적용되는 부분 다수)
|
||||
|
||||
### 2-1. 현재 패턴 분석
|
||||
|
||||
대부분의 actions.ts가 이 패턴을 따름:
|
||||
```typescript
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/endpoint`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
```
|
||||
|
||||
### 2-2. 자동 적용 범위 (Phase 1 완료 시)
|
||||
|
||||
Phase 1에서 `serverFetch`에 X-Tenant-ID를 자동 추가하면, **기존 actions.ts 대부분은 수정 없이** 테넌트 헤더가 전달됨.
|
||||
|
||||
### 2-3. 수동 확인 필요 케이스
|
||||
|
||||
`serverFetch`를 사용하지 않고 직접 `fetch()`를 호출하는 곳:
|
||||
```bash
|
||||
# 검색 대상
|
||||
grep -r "fetch(" src/components/*/actions.ts --include="*.ts" | grep -v serverFetch
|
||||
```
|
||||
|
||||
### 2-4. 선택적 URL 테넌트 프리픽스
|
||||
|
||||
백엔드가 URL 경로에 테넌트를 요구하는 경우만:
|
||||
```typescript
|
||||
// 필요한 경우에만 적용
|
||||
function buildTenantUrl(endpoint: string, tenantId?: string): string {
|
||||
if (endpoint.startsWith('http')) return endpoint; // 레거시 호환
|
||||
const base = process.env.NEXT_PUBLIC_API_URL;
|
||||
return tenantId
|
||||
? `${base}/api/v1/tenant/${tenantId}/${endpoint}`
|
||||
: `${base}/api/v1/${endpoint}`;
|
||||
}
|
||||
```
|
||||
|
||||
### 체크리스트
|
||||
|
||||
```
|
||||
- [ ] serverFetch 사용하지 않는 actions.ts 목록 확인
|
||||
- [ ] 직접 fetch() 호출하는 곳 serverFetch로 마이그레이션
|
||||
- [ ] 백엔드와 URL 패턴 vs 헤더 패턴 최종 협의
|
||||
- [ ] 고빈도 도메인 우선 검증: clients, items, production, sales
|
||||
- [ ] 에러 시 테넌트 컨텍스트 누락 로그 추가
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 포매터 & 유틸리티 테넌트 설정 기반 전환 🔴 `프론트 단독 가능`
|
||||
|
||||
> **목표**: 한국어 하드코딩된 포매터를 테넌트 설정 기반으로 변경
|
||||
> **예상**: 3-5일
|
||||
|
||||
### 3-1. 영향받는 파일 목록
|
||||
|
||||
| 파일 | 함수 | 하드코딩 내용 |
|
||||
|------|------|---------------|
|
||||
| `src/utils/formatAmount.ts` | `formatAmount()` | "원", "만원" |
|
||||
| `src/utils/formatAmount.ts` | `formatKoreanAmount()` | "억", "만" |
|
||||
| `src/lib/formatters.ts` | `formatBusinessNumber()` | 한국 사업자번호 (XXX-XX-XXXXX) |
|
||||
| `src/lib/formatters.ts` | `formatPhoneNumber()` | 한국 전화 (02-, 010-) |
|
||||
| `src/utils/date.ts` | `formatDate()` | `'ko-KR'` 로케일 |
|
||||
|
||||
### 3-2. TenantRegionalConfig 인터페이스
|
||||
|
||||
```typescript
|
||||
// src/types/tenant-config.ts (신규)
|
||||
export interface TenantRegionalConfig {
|
||||
locale: string; // 'ko-KR' | 'en-US' | 'ja-JP'
|
||||
timezone: string; // 'Asia/Seoul' | 'America/New_York'
|
||||
currency: {
|
||||
code: string; // 'KRW' | 'USD' | 'JPY'
|
||||
symbol: string; // '원' | '$' | '¥'
|
||||
locale: string; // Intl.NumberFormat 로케일
|
||||
largeUnitName?: string; // '만' (한국 전용)
|
||||
largeUnitValue?: number; // 10000
|
||||
};
|
||||
phone: {
|
||||
countryCode: string; // '+82' | '+1' | '+81'
|
||||
format: string; // 'XXX-XXXX-XXXX'
|
||||
};
|
||||
businessNumber: {
|
||||
format: string; // 'XXX-XX-XXXXX'
|
||||
label: string; // '사업자번호' | 'Business No.' | '法人番号'
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3-3. 마이그레이션 접근 (하위 호환)
|
||||
|
||||
기존 함수를 바로 변경하지 않고, 오버로드 + 기본값 패턴 적용:
|
||||
|
||||
```typescript
|
||||
// 기존 호출 코드를 깨지 않는 방식
|
||||
export function formatAmount(amount: number, config?: TenantCurrencyConfig): string {
|
||||
const cfg = config ?? DEFAULT_KR_CURRENCY_CONFIG; // 기본값: 한국
|
||||
// ... 테넌트 설정 기반 포매팅
|
||||
}
|
||||
```
|
||||
|
||||
### 3-4. 기존 공통화 작업 참조
|
||||
|
||||
**이미 작성된 관련 문서**:
|
||||
- `claudedocs/[IMPL-2026-02-05] formatter-commonization-plan.md`
|
||||
- `claudedocs/[ANALYSIS-2026-01-20] 공통화-현황-분석.md`
|
||||
|
||||
이 문서들의 포매터 공통화 계획과 병합하여 진행.
|
||||
|
||||
### 체크리스트
|
||||
|
||||
```
|
||||
- [ ] TenantRegionalConfig 인터페이스 정의
|
||||
- [ ] DEFAULT_KR_CONFIG 기본값 생성 (하위 호환)
|
||||
- [ ] formatAmount() 테넌트 설정 지원 추가
|
||||
- [ ] formatDate() 테넌트 로케일 지원 추가
|
||||
- [ ] formatBusinessNumber() 포맷 설정 지원 추가
|
||||
- [ ] formatPhoneNumber() 국가 코드 지원 추가
|
||||
- [ ] 기존 호출 코드 깨지지 않는지 검증
|
||||
- [ ] formatter-commonization-plan.md와 통합
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 브랜딩 동적화 🟡 `백엔드 API 필요`
|
||||
|
||||
> **목표**: 하드코딩된 회사명/로고를 테넌트 설정 기반으로 변경
|
||||
> **예상**: 2-3일
|
||||
|
||||
### 4-1. 영향받는 파일 목록
|
||||
|
||||
| 파일 | 하드코딩 | 변경 방향 |
|
||||
|------|----------|-----------|
|
||||
| `src/layouts/AuthenticatedLayout.tsx` | `APP_NAME = 'SAM'` | `tenant.company_name` 또는 테넌트 설정 |
|
||||
| `src/layouts/AuthenticatedLayout.tsx` | `<Image src="/sam-logo.png">` | 테넌트별 로고 URL |
|
||||
| `src/layouts/AuthenticatedLayout.tsx` | `MOCK_COMPANIES` 배열 | `user.tenant.other_tenants` 연동 |
|
||||
| `src/app/[locale]/layout.tsx` | `APP_TITLE = 'SAM - 내 손안의 대시보드'` | 테넌트 설정 기반 |
|
||||
| `src/app/[locale]/layout.tsx` | SEO 메타데이터 | 테넌트별 (단, 폐쇄형이므로 낮은 우선순위) |
|
||||
|
||||
### 4-2. 테넌트 브랜딩 설정 구조
|
||||
|
||||
```typescript
|
||||
// src/types/tenant-config.ts에 추가
|
||||
export interface TenantBrandingConfig {
|
||||
appName: string; // 'SAM' | '주일 MES' | 커스텀
|
||||
appSubtitle?: string; // 'Smart Automation Management'
|
||||
logoUrl: string; // '/sam-logo.png' | '/tenants/282/logo.png'
|
||||
faviconUrl?: string;
|
||||
primaryColor?: string; // 테마 주색상
|
||||
loginBackground?: string; // 로그인 페이지 배경
|
||||
}
|
||||
```
|
||||
|
||||
### 4-3. 적용 방식
|
||||
|
||||
```typescript
|
||||
// AuthenticatedLayout.tsx 내부
|
||||
const { currentUser } = useAuth();
|
||||
const branding = currentUser?.tenant?.branding ?? DEFAULT_BRANDING;
|
||||
|
||||
// 로고
|
||||
<Image src={branding.logoUrl} alt={branding.appName} />
|
||||
|
||||
// 앱 이름
|
||||
<h1>{branding.appName}</h1>
|
||||
```
|
||||
|
||||
### 4-4. MOCK_COMPANIES → other_tenants 연동
|
||||
|
||||
**현재**: 하드코딩 목업
|
||||
```typescript
|
||||
const MOCK_COMPANIES = [
|
||||
{ id: 'all', name: '전체' },
|
||||
{ id: 'company1', name: '(주)삼성건설' },
|
||||
...
|
||||
];
|
||||
```
|
||||
|
||||
**변경**: 실제 테넌트 데이터 연동
|
||||
```typescript
|
||||
const tenantOptions = useMemo(() => {
|
||||
const current = currentUser?.tenant;
|
||||
const others = current?.other_tenants ?? [];
|
||||
return [current, ...others].filter(Boolean);
|
||||
}, [currentUser]);
|
||||
```
|
||||
|
||||
### 체크리스트
|
||||
|
||||
```
|
||||
- [ ] TenantBrandingConfig 인터페이스 정의
|
||||
- [ ] DEFAULT_BRANDING 기본값 (현재 SAM 설정)
|
||||
- [ ] AuthenticatedLayout 로고/앱이름 동적화
|
||||
- [ ] MOCK_COMPANIES를 other_tenants 기반으로 교체
|
||||
- [ ] 로그인 페이지 브랜딩 동적화
|
||||
- [ ] favicon 동적 변경 (선택)
|
||||
- [ ] 테넌트별 로고 파일 서빙 방식 결정 (public/ vs API)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: 상수 & 비즈니스 로직 외부화 🟡 `백엔드 API 필요`
|
||||
|
||||
> **목표**: 한국 특화 상수를 테넌트/국가별 설정으로 외부화
|
||||
> **예상**: 3-5일
|
||||
|
||||
### 5-1. 영향받는 항목
|
||||
|
||||
| 항목 | 파일 | 현재 | 변경 |
|
||||
|------|------|------|------|
|
||||
| 공휴일 | `src/constants/calendarEvents.ts` | 한국 공휴일 하드코딩 | DB/API 기반 |
|
||||
| 프로세스 타입 | `src/types/process.ts` | "생산", "검사" 등 | i18n 라벨 |
|
||||
| 상태 라벨 | `src/lib/utils/status-config.ts` | "대기", "완료" 등 | i18n 라벨 |
|
||||
| 품목 타입 | `src/types/item.ts` | "제품", "부품" 등 | i18n 라벨 |
|
||||
| 근무일 | 관련 컴포넌트 | 월-금 하드코딩 | 테넌트 설정 |
|
||||
|
||||
### 5-2. 외부화 전략
|
||||
|
||||
**공휴일**: 백엔드 API로 이동 (테넌트별 국가 설정에 따라 반환)
|
||||
```typescript
|
||||
// AS-IS: 하드코딩
|
||||
const HOLIDAYS_2026 = [
|
||||
{ date: '2026-01-01', name: '신정', type: 'holiday' },
|
||||
...
|
||||
];
|
||||
|
||||
// TO-BE: API 호출
|
||||
const holidays = await getHolidays(tenantId, year);
|
||||
```
|
||||
|
||||
**라벨/상태**: next-intl 다국어 시스템 활용 (이미 ko/en/ja 구조 있음)
|
||||
```typescript
|
||||
// AS-IS
|
||||
const statusLabels = { pending: '대기', completed: '완료' };
|
||||
|
||||
// TO-BE
|
||||
const t = useTranslations('status');
|
||||
const label = t('pending'); // 로케일에 따라 자동 변환
|
||||
```
|
||||
|
||||
### 체크리스트
|
||||
|
||||
```
|
||||
- [ ] calendarEvents.ts 공휴일 데이터 → API 엔드포인트로 이동
|
||||
- [ ] 프로세스 타입 라벨 → messages/ko.json, en.json, ja.json으로 이동
|
||||
- [ ] 상태 라벨 → i18n 키로 변환
|
||||
- [ ] 품목 타입 라벨 → i18n 키로 변환
|
||||
- [ ] 근무일 설정 → 테넌트 config로 이동
|
||||
- [ ] 백엔드에 공휴일 API 요청
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: localStorage 잔재 정리 & 타입 통일 🟡 `프론트 단독 가능`
|
||||
|
||||
> **목표**: TenantAwareCache 미사용 곳 정리 + tenantId 타입 통일
|
||||
> **예상**: 2-3일
|
||||
|
||||
### 6-1. localStorage 직접 사용 감사
|
||||
|
||||
```bash
|
||||
# 검색 대상
|
||||
grep -r "localStorage\.\(setItem\|getItem\)" src/ --include="*.ts" --include="*.tsx"
|
||||
```
|
||||
|
||||
**알려진 비-테넌트-스코프 키**:
|
||||
- `mes-users` → 사용자 목록 (테넌트 스코프 필요 여부 검토)
|
||||
- `mes-currentUser` → 현재 사용자 (로그인 상태이므로 테넌트 무관)
|
||||
- 기타 직접 사용 곳 → TenantAwareCache 또는 테넌트 프리픽스 적용
|
||||
|
||||
### 6-2. tenantId 타입 통일
|
||||
|
||||
**현재 상황**:
|
||||
- `User.tenant.id` → `number` (AuthContext)
|
||||
- `PageConfig.tenantId` → `string` (masterDataStore)
|
||||
- TenantAwareCache → `number`
|
||||
|
||||
**통일**: `number`로 표준화 (백엔드 응답 기준)
|
||||
|
||||
```typescript
|
||||
// 수정 대상 찾기
|
||||
grep -r "tenantId.*string" src/ --include="*.ts"
|
||||
```
|
||||
|
||||
### 체크리스트
|
||||
|
||||
```
|
||||
- [ ] localStorage 직접 사용 전수 조사
|
||||
- [ ] TenantAwareCache로 마이그레이션 가능한 곳 목록화
|
||||
- [ ] 테넌트 스코프 불필요한 곳 명시 (mes-currentUser 등)
|
||||
- [ ] tenantId: string → number 통일
|
||||
- [ ] PageConfig 타입 수정
|
||||
- [ ] 관련 타입 참조 전부 업데이트
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: TenantConfigService & TenantContext (선택) 🟢 `백엔드 API 필요`
|
||||
|
||||
> **목표**: 테넌트 설정을 한곳에서 관리하는 서비스 레이어
|
||||
> **예상**: 3-5일 (Phase 3-5 진행 중 필요에 따라)
|
||||
|
||||
### 7-1. TenantConfigService
|
||||
|
||||
```typescript
|
||||
// src/services/TenantConfigService.ts (신규)
|
||||
export interface TenantConfiguration {
|
||||
tenantId: number;
|
||||
branding: TenantBrandingConfig;
|
||||
regional: TenantRegionalConfig;
|
||||
features: {
|
||||
enabledModules: string[];
|
||||
customFields?: Record<string, unknown>;
|
||||
};
|
||||
calendar: {
|
||||
workingDays: number[]; // [1,2,3,4,5] = 월-금
|
||||
holidays: HolidayEntry[];
|
||||
};
|
||||
}
|
||||
|
||||
class TenantConfigService {
|
||||
private cache: Map<number, TenantConfiguration> = new Map();
|
||||
|
||||
async getConfig(tenantId: number): Promise<TenantConfiguration> {
|
||||
if (this.cache.has(tenantId)) return this.cache.get(tenantId)!;
|
||||
const config = await this.fetchFromApi(tenantId);
|
||||
this.cache.set(tenantId, config);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7-2. TenantContext Provider
|
||||
|
||||
```typescript
|
||||
// src/contexts/TenantContext.tsx (신규)
|
||||
export function TenantProvider({ children }: { children: ReactNode }) {
|
||||
const { currentUser } = useAuth();
|
||||
const [config, setConfig] = useState<TenantConfiguration>();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser?.tenant?.id) {
|
||||
tenantConfigService.getConfig(currentUser.tenant.id)
|
||||
.then(setConfig);
|
||||
}
|
||||
}, [currentUser?.tenant?.id]);
|
||||
|
||||
return (
|
||||
<TenantContext.Provider value={config}>
|
||||
{children}
|
||||
</TenantContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// 사용
|
||||
const tenantConfig = useTenantConfig();
|
||||
const currencySymbol = tenantConfig.regional.currency.symbol;
|
||||
```
|
||||
|
||||
### 체크리스트
|
||||
|
||||
```
|
||||
- [ ] TenantConfiguration 통합 인터페이스 설계
|
||||
- [ ] TenantConfigService 구현 (캐시 + API 호출)
|
||||
- [ ] TenantContext Provider 구현
|
||||
- [ ] useTenantConfig() 훅 구현
|
||||
- [ ] Protected Layout에 TenantProvider 추가
|
||||
- [ ] 기존 코드에서 점진적 마이그레이션
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: 테넌트 라우팅 (필요 시) 🟢 `프론트 단독 가능`
|
||||
|
||||
> **목표**: URL에 테넌트 식별자 포함 (필요한 경우에만)
|
||||
> **예상**: 1주+
|
||||
|
||||
### 현재 라우팅
|
||||
```
|
||||
/[locale]/(protected)/dashboard
|
||||
```
|
||||
|
||||
### 옵션 A: 경로 기반 (권장 - 필요 시)
|
||||
```
|
||||
/[tenant]/[locale]/(protected)/dashboard
|
||||
/acme/ko/dashboard
|
||||
```
|
||||
|
||||
### 옵션 B: 서브도메인 기반
|
||||
```
|
||||
acme.sam.com/ko/dashboard
|
||||
```
|
||||
|
||||
### 옵션 C: 현재 유지 (인증 기반만)
|
||||
```
|
||||
/[locale]/(protected)/dashboard ← 테넌트는 JWT/쿠키로만 식별
|
||||
```
|
||||
|
||||
**결정**: 현재는 **옵션 C 유지**. 다중 테넌트 URL 분리가 필요해지면 옵션 A 도입.
|
||||
|
||||
---
|
||||
|
||||
## 백엔드 협의 사항
|
||||
|
||||
### 필수 협의 (Phase 1 시작 전)
|
||||
|
||||
| 항목 | 질문 | 결정 사항 |
|
||||
|------|------|-----------|
|
||||
| 테넌트 식별 방식 | `X-Tenant-ID` 헤더 vs URL 경로 vs JWT만? | TBD |
|
||||
| X-Tenant-ID 수신 | 백엔드가 이 헤더를 읽고 필터링하는지? | TBD |
|
||||
| JWT 내 tenant_id | 토큰에 tenant_id가 포함되어 있는지? | TBD |
|
||||
| 공휴일 API | `GET /api/v1/holidays?year=2026` 지원? | TBD |
|
||||
| 테넌트 설정 API | `GET /api/v1/tenant/config` 지원? | TBD |
|
||||
|
||||
### 선택 협의 (Phase 4-5 시작 전)
|
||||
|
||||
| 항목 | 질문 | 결정 사항 |
|
||||
|------|------|-----------|
|
||||
| 테넌트 로고 | 로고 URL을 어디서 제공? (API vs 파일서버) | TBD |
|
||||
| 브랜딩 설정 | 테넌트별 앱이름/테마 API 제공 가능? | TBD |
|
||||
| 다국어 라벨 | 백엔드 코드 라벨이 다국어 지원? | TBD |
|
||||
|
||||
---
|
||||
|
||||
## 실행 우선순위 요약
|
||||
|
||||
```
|
||||
[프론트 단독] Phase 3: 포매터 테넌트 설정 기반 🔴 3-5일 ← 즉시 시작 가능
|
||||
[프론트 단독] Phase 6: localStorage 정리/타입 통일 🟡 2-3일 ← 즉시 시작 가능
|
||||
[프론트 단독] Phase 8: 테넌트 라우팅 🟢 필요시 ← 당분간 불필요
|
||||
|
||||
[백엔드 협의] Phase 1: API 테넌트 컨텍스트 주입 🔴 3-5일 ← 백엔드 확인 후
|
||||
[백엔드 협의] Phase 2: Server Actions 마이그레이션 🔴 1-2주 ← Phase 1 후 자동 적용 범위 큼
|
||||
|
||||
[백엔드 API] Phase 4: 브랜딩 동적화 🟡 2-3일 ← 테넌트 설정 API 필요
|
||||
[백엔드 API] Phase 5: 상수/공휴일 외부화 🟡 3-5일 ← 공휴일 API 필요
|
||||
[백엔드 API] Phase 7: TenantConfigService 🟢 3-5일 ← 통합 설정 API 필요
|
||||
```
|
||||
|
||||
### 병렬 진행 가능 조합
|
||||
|
||||
```
|
||||
[즉시 시작 - 프론트 단독]
|
||||
├─ Phase 3 (포매터) ─────────→ 독립 완료
|
||||
└─ Phase 6 (localStorage) ──→ 독립 완료
|
||||
|
||||
[백엔드 협의 후 - 프론트+백엔드]
|
||||
└─ Phase 1 (API 헤더) ──────→ Phase 2 (Actions 자동 적용)
|
||||
|
||||
[백엔드 API 준비 후 - 프론트+백엔드]
|
||||
└─ Phase 7 (TenantConfig) ──→ Phase 4 (브랜딩) + Phase 5 (상수)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 위험 요소 & 대응
|
||||
|
||||
| 위험 | 확률 | 영향 | 대응 |
|
||||
|------|------|------|------|
|
||||
| 70+ actions.ts 수동 마이그레이션 | 높음 | 중간 | serverFetch 자동 주입으로 대부분 해결 |
|
||||
| 백엔드 X-Tenant-ID 미지원 | 중간 | 높음 | Phase 1 시작 전 백엔드 팀 협의 필수 |
|
||||
| 포매터 변경 시 기존 UI 깨짐 | 낮음 | 중간 | 기본값 패턴으로 하위 호환 유지 |
|
||||
| 캐시 무효화 누락 | 낮음 | 높음 | TenantAwareCache 이미 검증됨 |
|
||||
| 다국어 번역 리소스 부족 | 중간 | 낮음 | 한국어 기본값 유지, 점진 추가 |
|
||||
|
||||
---
|
||||
|
||||
## 관련 문서
|
||||
|
||||
| 문서 | 설명 |
|
||||
|------|------|
|
||||
| `architecture/[REF-2025-11-19] multi-tenancy-implementation.md` | 이전 멀티테넌시 구현 (Phase 1-2 → 완료됨) |
|
||||
| `architecture/[TEST-2025-11-19] multi-tenancy-test-guide.md` | 캐시 격리 테스트 가이드 |
|
||||
| `architecture/[FIX-2026-01-29] masterdata-cache-tenant-isolation.md` | masterDataStore 캐시 테넌트 격리 수정 |
|
||||
| `[IMPL-2026-02-05] formatter-commonization-plan.md` | 포매터 공통화 계획 (Phase 3과 병합) |
|
||||
| `[ANALYSIS-2026-01-20] 공통화-현황-분석.md` | 공통화 현황 분석 |
|
||||
| `[ANALYSIS-2026-02-05] list-page-commonization-status.md` | 리스트 페이지 공통화 현황 |
|
||||
| `auth/[IMPL-2025-11-07] jwt-cookie-authentication-final.md` | JWT 쿠키 인증 구현 |
|
||||
| `api/[REF] api-requirements.md` | API 요구사항 |
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
|
||||
| 날짜 | 변경 내용 |
|
||||
|------|-----------|
|
||||
| 2026-02-06 | 초기 작성 - 전체 코드베이스 분석 기반 8 Phase 로드맵 |
|
||||
|
||||
---
|
||||
|
||||
**다음 액션**: Phase 1 시작 전 백엔드 팀과 `X-Tenant-ID` 헤더 수신 방식 협의
|
||||
446
claudedocs/architecture/[PLAN-2026-02-06] refactoring-roadmap.md
Normal file
446
claudedocs/architecture/[PLAN-2026-02-06] refactoring-roadmap.md
Normal file
@@ -0,0 +1,446 @@
|
||||
# 리팩토링 로드맵
|
||||
|
||||
**작성일**: 2026-02-06
|
||||
**목적**: 전체 코드베이스 리팩토링 포인트 점검 및 실행 계획
|
||||
**상태**: Phase 1 완료, Phase 3 완료 (공용 유틸 추출), Phase 4 SearchableSelectionModal 완료
|
||||
|
||||
---
|
||||
|
||||
## 현재 코드베이스 수치 (2026-02-06 기준)
|
||||
|
||||
| 지표 | 수치 | 비고 |
|
||||
|------|------|------|
|
||||
| 전체 코드 | ~301,000줄 | TS/TSX |
|
||||
| 컴포넌트 파일 | ~551개 | |
|
||||
| 페이지 파일 | ~253개 | |
|
||||
| action.ts 파일 | 80개 | 거의 동일 CRUD 패턴 |
|
||||
| types.ts 파일 | 94개 | 중복 타입 다수 |
|
||||
| 모달 컴포넌트 | 42개 | 유사 패턴 반복 |
|
||||
| 2000줄+ 파일 | 4개 | God 컴포넌트 |
|
||||
| 1000~2000줄 파일 | 25+개 | 분리 대상 |
|
||||
| 500~1000줄 파일 | 50+개 | 검토 대상 |
|
||||
|
||||
---
|
||||
|
||||
## God 컴포넌트 / 대형 파일 목록
|
||||
|
||||
### 🔴 2000줄 이상 (즉시 분리 필요)
|
||||
|
||||
| 파일 | 줄수 | 핵심 문제 | 분리 방향 |
|
||||
|------|------|-----------|-----------|
|
||||
| `components/business/MainDashboard.tsx` | 2,651 | CEO/영업/생산/품질 대시보드 한 파일 | 역할별 섹션 컴포넌트 분리 |
|
||||
| `contexts/ItemMasterContext.tsx` | 2,406 | useState 17개, useEffect 15개, 함수 50+개 | 도메인별 5개 Context 분리 |
|
||||
| `lib/api/item-master.ts` | 2,232 | 모든 품목 API 한 파일 | 도메인별 API 모듈 분리 |
|
||||
| `lib/api/dashboard/transformers.ts` | 1,576 | 전체 대시보드 변환 로직 | 섹션별 transformer 분리 |
|
||||
|
||||
### 🟡 1000~2000줄 (우선 검토)
|
||||
|
||||
| 파일 | 줄수 | 도메인 | 분리 방향 |
|
||||
|------|------|--------|-----------|
|
||||
| `components/orders/actions.ts` | 1,394 | 수주 | 서비스 레이어 분리 |
|
||||
| `components/accounting/ExpectedExpenseManagement/index.tsx` | 1,299 | 회계 | 서브 컴포넌트 추출 |
|
||||
| `layouts/AuthenticatedLayout.tsx` | 1,289 | 레이아웃 | 훅 24개 → 섹션별 분리 |
|
||||
| `components/quotes/QuoteRegistration.tsx` | 1,268 | 견적 | 폼 섹션 추출, useState 13개 |
|
||||
| `components/quotes/actions.ts` | 1,266 | 견적 | API 레이어 분리 |
|
||||
| `components/business/construction/management/actions.ts` | 1,222 | 건설 | 도메인 서비스 추출 |
|
||||
| `components/business/construction/estimates/actions.ts` | 1,222 | 건설 | 도메인 서비스 추출 |
|
||||
| `components/production/WorkerScreen/index.tsx` | 1,198 | 생산 | 화면 섹션 분리 |
|
||||
| `hooks/useCEODashboard.ts` | 1,172 | 대시보드 | useState 18개 → 섹션별 훅 분리 |
|
||||
| `components/material/ReceivingManagement/actions.ts` | 1,152 | 자재 | API 서비스 레이어 |
|
||||
| `components/quotes/types.ts` | 1,149 | 견적 | 타입 조직화 |
|
||||
| `components/quality/InspectionManagement/InspectionDetail.tsx` | 1,125 | 품질 | 컴포넌트 추출 |
|
||||
| `components/hr/VacationManagement/actions.ts` | 1,125 | HR | 서비스 레이어 분리 |
|
||||
| `components/orders/OrderRegistration.tsx` | 1,123 | 수주 | 폼 섹션 추출, useState 12개 |
|
||||
| `components/items/DynamicItemForm/index.tsx` | 1,073 | 품목 | 복합 폼 로직 추출 |
|
||||
| `components/templates/IntegratedListTemplateV2.tsx` | 1,066 | 템플릿 | 템플릿 특화 |
|
||||
| `components/hr/EmployeeManagement/EmployeeForm.tsx` | 1,051 | HR | 폼 섹션 분리 |
|
||||
| `components/quotes/QuoteRegistrationV2.tsx` | 1,020 | 견적 | 폼 리팩토링 |
|
||||
| `components/templates/UniversalListPage/index.tsx` | 1,007 | 템플릿 | 템플릿 최적화 |
|
||||
| `components/items/ItemMasterDataManagement.tsx` | 1,005 | 품목 | 도메인 로직 추출 |
|
||||
|
||||
---
|
||||
|
||||
## 중복 패턴 분석
|
||||
|
||||
### 1. 액션 파일 80개 동일 패턴 (~24,000줄 중복)
|
||||
|
||||
**현재**: 모든 도메인이 이 구조를 복붙
|
||||
```typescript
|
||||
'use server';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
|
||||
interface Api[Domain]Data { ... } // 타입 정의 100~300줄
|
||||
function transform(data) { ... } // API→프론트 변환 50~100줄
|
||||
|
||||
export async function getList(params) { // 목록 조회
|
||||
const url = `${API_URL}/api/v1/endpoint`;
|
||||
const { response } = await serverFetch(url, { method: 'GET' });
|
||||
return transform(response);
|
||||
}
|
||||
export async function getById(id) { ... } // 상세 조회
|
||||
export async function create(data) { ... } // 생성
|
||||
export async function update(id, data) { ... } // 수정
|
||||
export async function delete(id) { ... } // 삭제
|
||||
export async function bulkDelete(ids) { ... } // 일괄 삭제
|
||||
```
|
||||
|
||||
**해당 도메인**: orders, quotes, clients, accounting(13모듈), hr(6모듈), production(4모듈), material(2모듈), quality(2모듈), construction(17모듈), settings(14모듈)
|
||||
|
||||
**해결 방향**: 제네릭 API 서비스 팩토리
|
||||
```typescript
|
||||
// lib/api/createCrudService.ts
|
||||
function createCrudService<TApi, TFront>(config: {
|
||||
endpoint: string;
|
||||
transform: (api: TApi) => TFront;
|
||||
reverseTransform: (front: TFront) => Partial<TApi>;
|
||||
}) {
|
||||
return {
|
||||
getList: async (params) => { ... },
|
||||
getById: async (id) => { ... },
|
||||
create: async (data) => { ... },
|
||||
update: async (id, data) => { ... },
|
||||
delete: async (id) => { ... },
|
||||
bulkDelete: async (ids) => { ... },
|
||||
};
|
||||
}
|
||||
|
||||
// 사용: 10줄로 끝
|
||||
const orderService = createCrudService<ApiOrder, Order>({
|
||||
endpoint: 'orders',
|
||||
transform: transformOrder,
|
||||
reverseTransform: reverseTransformOrder,
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 데이터 페칭 패턴 3가지 혼재
|
||||
|
||||
| 패턴 | 사용 비율 | 위치 |
|
||||
|------|-----------|------|
|
||||
| useEffect + .then() 직접 호출 | ~75% (99+ 컴포넌트) | 대부분의 도메인 |
|
||||
| 커스텀 훅 (useDetailData 등) | ~15% (~15 컴포넌트) | 신규 구현 |
|
||||
| ApiClient 클래스 | ~10% (15 컴포넌트) | 건설 도메인만 |
|
||||
|
||||
**수동 로딩 상태 관리**: 262곳에서 반복
|
||||
```typescript
|
||||
// 이 패턴이 262번 반복됨
|
||||
const [data, setData] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
fetchData()
|
||||
.then(result => setData(result))
|
||||
.catch(err => setError(err))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
```
|
||||
|
||||
### 3. 폼 검증 3가지 방식 혼재
|
||||
|
||||
| 방식 | 사용 파일 수 | 비율 |
|
||||
|------|-------------|------|
|
||||
| Zod 스키마 (정석) | 3개 (로그인, 회원가입, 품목) | 5% |
|
||||
| 수동 if문 검증 | 50+개 | 60% |
|
||||
| 검증 없음 | ~30개 | 35% |
|
||||
|
||||
### 4. 리스트 페이지 템플릿 이중화
|
||||
|
||||
| 방식 | 사용 | 비율 |
|
||||
|------|------|------|
|
||||
| `UniversalListPage` (신규 표준) | 20개 페이지 | 25% |
|
||||
| 수동 구현 (레거시) | 60+ 페이지 | 75% |
|
||||
|
||||
### 5. 모달/다이얼로그 42개 유사 패턴
|
||||
|
||||
**검색/선택 모달 5개+ 거의 동일**:
|
||||
- `quotes/ItemSearchModal.tsx`
|
||||
- `production/WorkOrders/AssigneeSelectModal.tsx`
|
||||
- `material/ReceivingManagement/SupplierSearchModal.tsx`
|
||||
- `quality/InspectionManagement/OrderSelectModal.tsx`
|
||||
- `production/WorkOrders/SalesOrderSelectModal.tsx`
|
||||
|
||||
전부 "검색 입력 → API 호출 → 목록 표시 → 체크박스 선택 → 확인" 동일 구조
|
||||
→ `SearchableSelectionModal<T>` 하나로 통합 가능
|
||||
|
||||
---
|
||||
|
||||
## 성능 최적화 포인트
|
||||
|
||||
| 항목 | 현재 상태 | 영향도 | 해결 방향 |
|
||||
|------|-----------|--------|-----------|
|
||||
| React.memo | 551개 컴포넌트 중 **1개만** 사용 | 🔴 높음 | 리스트 아이템/카드 컴포넌트에 적용 |
|
||||
| 인라인 화살표 함수 | **746곳** `onClick={() => ...}` | 🟡 중간 | 대형 컴포넌트에서 useCallback 적용 |
|
||||
| useMemo 미사용 | 대용량 배열 필터링/정렬 곳곳 | 🟡 중간 | 비용 높은 계산에 적용 |
|
||||
|
||||
**React.memo 우선 적용 대상** (리스트 내 반복 렌더링 컴포넌트):
|
||||
- `production/WorkerScreen/WorkItemCard.tsx`
|
||||
- `board/CommentSection/CommentItem.tsx`
|
||||
- `business/construction/management/ProjectCard.tsx`
|
||||
- 기타 *Row, *Item, *Card 컴포넌트 30+개
|
||||
|
||||
---
|
||||
|
||||
## 타입 시스템 문제
|
||||
|
||||
| 항목 | 수치 | 영향 |
|
||||
|------|------|------|
|
||||
| `any` 타입 사용 | 102곳 (29개 파일) | 타입 안전성 저하 |
|
||||
| 동일 엔티티 다중 타입 정의 | Vendor, Item, Order 등 | 변환 코드 ~800줄 중복 |
|
||||
| types.ts 파일 | 94개 | 정규 타입 찾기 어려움 |
|
||||
| @ts-ignore/eslint-disable | 25개 파일 | 숨겨진 타입 에러 |
|
||||
|
||||
---
|
||||
|
||||
## 추출 가능한 공통 훅 목록
|
||||
|
||||
### 즉시 생성 가능 (프론트 단독)
|
||||
|
||||
| 훅 이름 | 대체 범위 | 예상 절감 | 기존 참고 |
|
||||
|---------|-----------|-----------|-----------|
|
||||
| `useListData` | 60+ 리스트 페이지 | ~4,000줄 | hooks/useDetailData.ts 패턴 확장 |
|
||||
| `useFormSubmit` | 80+ 폼 | ~3,000줄 | 신규 |
|
||||
| `usePagination` | 60+ 컴포넌트 | ~1,000줄 | 신규 |
|
||||
| `useModal<T>` | 42 모달 | ~500줄 | 신규 |
|
||||
| `useClientSideFiltering` | 55+ 컴포넌트 | ~800줄 | 신규 |
|
||||
|
||||
### 기존 훅 (활용 확대 필요)
|
||||
|
||||
| 훅 | 현재 사용 | 전체 적용 시 |
|
||||
|----|-----------|-------------|
|
||||
| `useDetailData` | ~15 컴포넌트 | 100+ 상세 페이지 |
|
||||
| `useDetailPageState` | ~10 컴포넌트 | 100+ 상세 페이지 |
|
||||
| `useCRUDHandlers` | ~10 컴포넌트 | 80+ CRUD 페이지 |
|
||||
|
||||
---
|
||||
|
||||
## 실행 계획
|
||||
|
||||
### Phase 1: 공통 훅 추출 ✅ 완료 (2026-02-09)
|
||||
|
||||
> 실제 코드 분석 결과 계획 수정 → 실증 기반 리팩토링 실행
|
||||
|
||||
**실행 결과** (계획 vs 실제):
|
||||
```
|
||||
기존 계획의 useListData, usePagination, useClientSideFiltering, useModal은
|
||||
UniversalListPage 템플릿이 이미 내부 처리 → 불필요 판정.
|
||||
|
||||
실제 실행:
|
||||
- [x] Step 1: executeServerAction (82개 action.ts 에러처리 래퍼) → ~3,000줄 절감
|
||||
- [x] Step 2: useDeleteDialog (6개 파일 삭제 다이얼로그 통합) → ~150줄 절감
|
||||
- [x] Step 3: useStatsLoader (7개 파일 stats 로딩 통합) → ~100줄 절감
|
||||
- [x] Step 4: React.memo 3개 + any→unknown 7건 + @ts-ignore 0건
|
||||
```
|
||||
|
||||
**실제 효과**: ~3,750줄 절감, 82개 action.ts 패턴 통일, 타입 안전성 향상
|
||||
**상세**: `refactoring/[IMPL-2026-02-09] phase1-common-hooks-checklist.md`
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: God 컴포넌트 분리 (2-3주) `프론트 단독`
|
||||
|
||||
> 2000줄+ 파일 4개 + 핵심 1000줄+ 파일 우선 분리
|
||||
|
||||
**작업 항목**:
|
||||
```
|
||||
- [ ] MainDashboard.tsx (2,651줄) 분리
|
||||
→ sections/CEOSection, SalesSection, ProductionSection, QualitySection
|
||||
→ hooks/useDashboardData
|
||||
→ utils/calculations
|
||||
- [ ] ItemMasterContext.tsx (2,406줄) 분리
|
||||
→ ItemContext, SpecificationContext, MaterialContext
|
||||
→ TemplateContext, AttributeContext
|
||||
- [ ] useCEODashboard.ts (1,172줄) 분리
|
||||
→ useDailyReport, useReceivables, useMonthlyExpense 등 개별 훅
|
||||
→ 훅 팩토리 패턴 적용
|
||||
- [ ] lib/api/item-master.ts (2,232줄) 분리
|
||||
→ 도메인별 API 모듈 (items, specifications, materials, templates)
|
||||
- [ ] AuthenticatedLayout.tsx (1,289줄)
|
||||
→ useLayoutState, useNavigation, useTenantBranding 훅 추출
|
||||
```
|
||||
|
||||
**예상 효과**: 유지보수성 +50%, 단위 테스트 가능성 확보
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: 액션 파일 공용 유틸 추출 ✅ 완료 (2026-02-10)
|
||||
|
||||
> 전수 분석 → 팩토리 ROI 재평가 → 공용 유틸 추출로 전략 변경
|
||||
|
||||
**전수 분석 결과** (82개 action 파일):
|
||||
```
|
||||
- 35개: executeServerAction 패턴 (Phase 1에서 통일)
|
||||
- 15개: 모의 데이터 (mock, API 미연동)
|
||||
- 13개: ApiClient 클래스 패턴 (건설 도메인)
|
||||
- 나머지: 특수 도메인 로직 (견적, 수주, 품목 등)
|
||||
```
|
||||
|
||||
**팩토리 마이그레이션 ROI 재평가**:
|
||||
```
|
||||
- createCrudService 팩토리: 2개(Rank, Title)만 적합 → ROI ~6% (너무 낮음)
|
||||
- 대부분 파일: 페이지네이션, 커스텀 쿼리 파라미터, 도메인 특화 로직으로 팩토리 패턴 부적합
|
||||
- 결론: 팩토리 대량 마이그레이션 대신 공용 유틸 추출로 전략 전환
|
||||
```
|
||||
|
||||
**실행 결과** (2026-02-10):
|
||||
```
|
||||
Step 1: 공용 타입 추출 (src/lib/api/types.ts)
|
||||
- [x] PaginatedApiResponse<T> — 25+ 파일에서 중복 정의 제거
|
||||
- [x] PaginationMeta, PaginatedResult<T> — 프론트엔드 표준 페이지네이션 타입
|
||||
- [x] toPaginationMeta() — snake_case → camelCase 변환 헬퍼
|
||||
- [x] SelectOption — 공용 선택 옵션 타입
|
||||
|
||||
Step 2: 공용 룩업 헬퍼 추출 (src/lib/api/shared-lookups.ts)
|
||||
- [x] fetchVendorOptions() — 거래처 목록 조회 (4개 파일 중복 제거)
|
||||
- [x] fetchBankAccountOptions() — 계좌 목록 조회 (심플)
|
||||
- [x] fetchBankAccountDetailOptions() — 계좌 상세 조회 (bankName, accountNumber 포함)
|
||||
- [x] BankAccountOption 타입
|
||||
|
||||
Step 3: PaginatedResponse 타입 마이그레이션 (~20개 파일)
|
||||
- [x] 제네릭 패턴 (interface PaginatedResponse<T>) → import PaginatedApiResponse
|
||||
- [x] 도메인 패턴 (interface XxxPaginatedResponse) → type alias
|
||||
- 스킵: VendorManagement/types.ts (page?/size? 비표준), PermissionManagement/types.ts (meta 래퍼)
|
||||
|
||||
Step 4: 공용 룩업 헬퍼 마이그레이션 (4개 파일)
|
||||
- [x] DepositManagement/actions.ts — getVendors + getBankAccounts 교체
|
||||
- [x] WithdrawalManagement/actions.ts — getVendors + getBankAccounts 교체
|
||||
- [x] PurchaseManagement/actions.ts — getVendors + getBankAccounts(상세) 교체
|
||||
- [x] ExpectedExpenseManagement/actions.ts — getBankAccounts(상세) 교체
|
||||
|
||||
Step 5: TypeScript 검증 통과 ✅
|
||||
```
|
||||
|
||||
**실측 효과**:
|
||||
- PaginatedResponse 중복 제거: ~20개 파일, 파일당 ~7줄 = ~140줄 절감
|
||||
- 공용 룩업 헬퍼: 4개 파일, 파일당 ~20줄 = ~80줄 절감
|
||||
- 총 ~220줄 직접 절감 + 향후 새 파일에서 중복 방지
|
||||
- createCrudService + TitleManagement 마이그레이션: ~36줄 절감 (프로토타입 포함)
|
||||
|
||||
**생성된 공용 파일**:
|
||||
- `src/lib/api/types.ts` — 공용 API 타입 (PaginatedApiResponse, PaginationMeta 등)
|
||||
- `src/lib/api/shared-lookups.ts` — 공용 룩업 헬퍼 (fetchVendorOptions 등)
|
||||
- `src/lib/api/create-crud-service.ts` — CRUD 팩토리 (Rank, Title 2개 사용)
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: 템플릿/패턴 통일 (2-3주) `프론트 단독` `SearchableSelectionModal 완료`
|
||||
|
||||
> UniversalListPage 확대 + 검증 표준화 + 모달 통합
|
||||
|
||||
**SearchableSelectionModal 완료** (2026-02-10):
|
||||
```
|
||||
- [x] SearchableSelectionModal<T> 공통 컴포넌트 생성
|
||||
- types.ts, useSearchableData.ts, SearchableSelectionModal.tsx, index.ts
|
||||
- 단일선택(single) + 다중선택(multiple) + listWrapper(테이블용) 지원
|
||||
- [x] ItemSearchModal 교체 (212→113줄, -47%)
|
||||
- [x] SupplierSearchModal 교체 (268→161줄, -40%)
|
||||
- [x] SalesOrderSelectModal 교체 (163→101줄, -38%)
|
||||
- [x] QuotationSelectDialog 교체 (196→113줄, -42%)
|
||||
- [x] OrderSelectModal 교체 (220→107줄, -51%)
|
||||
- [x] organisms/index.ts export 추가
|
||||
- [x] CLAUDE.md 공통 컴포넌트 사용 규칙 + claudedocs 가이드 문서 작성
|
||||
```
|
||||
**실측 효과**: 1,059줄 → 595줄 (464줄 절감, -44%) + 공통 컴포넌트 ~430줄
|
||||
|
||||
**남은 작업**:
|
||||
```
|
||||
- [ ] UniversalListPage 기능 보강
|
||||
- 고급 필터 UI
|
||||
- 컬럼 커스터마이징
|
||||
- 내보내기 기능
|
||||
- [ ] 레거시 리스트 페이지 → UniversalListPage 마이그레이션 (우선 20개)
|
||||
- [ ] Zod 검증 스키마 라이브러리 구축
|
||||
- lib/validations/common.ts (이메일, 전화, 사업자번호)
|
||||
- lib/validations/vendor.ts, order.ts, item.ts 등
|
||||
- [ ] 수동 검증 50+ 폼 → Zod 마이그레이션 (우선 10개)
|
||||
```
|
||||
|
||||
**예상 효과**: ~5,000줄 절감 (SearchableSelectionModal ~464줄 달성), UX 일관성 +80%
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: 성능 + 타입 정리 (1-2주) `프론트 단독` `일부 Phase 1에서 선처리`
|
||||
|
||||
> React.memo 적용 + any 제거 + 타입 통합
|
||||
|
||||
**Phase 1에서 선처리된 항목** (2026-02-09):
|
||||
```
|
||||
- [x] React.memo 3개 적용 (InfoField, CommentItem, WorkItemCard)
|
||||
- [x] any→unknown 7건 (logger.ts)
|
||||
- [x] action error handler any 50+곳 (executeServerAction으로 자동 해결)
|
||||
- [x] @ts-ignore 0건 (이미 제거 완료)
|
||||
```
|
||||
|
||||
**남은 작업**:
|
||||
```
|
||||
- [ ] React.memo 추가 적용 (나머지 리스트 아이템 컴포넌트)
|
||||
- [ ] 대형 컴포넌트 useCallback 적용
|
||||
- [ ] any 타입 잔여 92건
|
||||
- items/ 도메인 60건 (복잡도 높음, 별도 작업)
|
||||
- Form 에러 캐스팅 26건 (RHF 타입 시스템 변경 필요)
|
||||
- dev/ 프로토타입 6건 (비프로덕션)
|
||||
- [ ] 공통 타입 라이브러리 정리
|
||||
- types/shared/ 폴더 생성
|
||||
- PaginatedApiResponse<T> ✅ Phase 3에서 완료 (src/lib/api/types.ts)
|
||||
- FormState<T>, SelectOption 등 추가 타입
|
||||
```
|
||||
|
||||
**예상 효과**: 리스트 렌더링 30-50% 개선, 타입 안전성 +60%
|
||||
|
||||
---
|
||||
|
||||
## 전체 예상 효과 요약
|
||||
|
||||
| 지표 | Phase 1 ✅ | Phase 2 | Phase 3 ✅ | Phase 4 | Phase 5 | 합계 |
|
||||
|------|-----------|---------|---------|---------|---------|------|
|
||||
| 코드 절감 | ~3,750줄 (실측) | (구조 개선) | ~256줄 (실측) | ~5,000줄 | (품질 개선) | **~9,000줄+** |
|
||||
| 중복 제거 | 82개 action 통일 | - | 25+ 타입 + 4 룩업 통합 | 5 모달 통합 | - | 종합 개선 |
|
||||
| 패턴 일관성 | +60% | +50% | +30% (타입 표준화) | +80% | +60% | 종합 개선 |
|
||||
| 유지보수성 | 높음 | 매우 높음 | 중간 (공용 유틸) | 중간 | 중간 | 종합 개선 |
|
||||
| 위험도 | 낮음 | 중간 | 낮음 (완료) | 낮음 | 낮음 | - |
|
||||
|
||||
---
|
||||
|
||||
## 병렬 진행 가능 조합
|
||||
|
||||
```
|
||||
[완료]
|
||||
├─ Phase 1 (공통 훅) ──────→ ✅ 완료 (2026-02-09)
|
||||
├─ Phase 3 (공용 유틸 추출) ──→ ✅ 완료 (2026-02-10)
|
||||
├─ Phase 4 (SearchableSelectionModal) → ✅ 완료 (2026-02-10)
|
||||
│
|
||||
[즉시 시작 가능]
|
||||
├─ Phase 2 (God 컴포넌트 분리) ──→ Phase 1 훅 + Phase 3 공용 타입 활용
|
||||
├─ Phase 4 남은 작업 (UniversalListPage 확대, Zod 검증)
|
||||
├─ Phase 5 (성능/타입) ─────→ 일부 Phase 1/3에서 선처리됨
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 관련 문서
|
||||
|
||||
| 문서 | 설명 |
|
||||
|------|------|
|
||||
| `[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md` | 멀티테넌시 공통화 로드맵 (별도 트랙) |
|
||||
| `[ANALYSIS-2026-01-20] 공통화-현황-분석.md` | 공통화 현황 분석 |
|
||||
| `[ANALYSIS-2026-02-05] list-page-commonization-status.md` | 리스트 페이지 공통화 현황 |
|
||||
| `[IMPL-2026-02-05] detail-hooks-migration-plan.md` | 상세 페이지 훅 마이그레이션 계획 |
|
||||
| `[IMPL-2026-02-05] formatter-commonization-plan.md` | 포매터 공통화 계획 |
|
||||
| `[PLAN-2026-01-22] ui-component-abstraction.md` | UI 컴포넌트 추상화 계획 |
|
||||
| `guides/[PLAN-2025-12-23] common-component-extraction-plan.md` | 공통 컴포넌트 추출 계획 |
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
|
||||
| 날짜 | 변경 내용 |
|
||||
|------|-----------|
|
||||
| 2026-02-06 | 초기 작성 - 전체 코드베이스 분석 기반 5 Phase 로드맵 |
|
||||
| 2026-02-09 | Phase 1 완료 반영 - 실측 기반 효과 수치 보정 (8,500줄→3,750줄), executeServerAction/useDeleteDialog/useStatsLoader 3개 훅 생성 완료 |
|
||||
| 2026-02-09 | Phase 3 프로토타입 검증 완료 - createCrudService 팩토리 생성, RankManagement 5/5 CRUD 정상, Server Action 호환성 확인 |
|
||||
| 2026-02-10 | Phase 4 SearchableSelectionModal 완료 - 5개 모달 통합, 464줄 절감(-44%), 가이드 문서 작성 |
|
||||
| 2026-02-10 | Phase 3 완료 - 전수 분석 후 팩토리 ROI 재평가(~6%), 공용 유틸 추출로 전략 전환. PaginatedApiResponse 25+파일 타입 통합, 공용 룩업 헬퍼 4파일 중복 제거, ~256줄 절감 |
|
||||
|
||||
---
|
||||
|
||||
**모든 Phase 프론트 단독 가능** - 백엔드 의존성 없음
|
||||
316
claudedocs/architecture/[REF] technical-decisions.md
Normal file
316
claudedocs/architecture/[REF] technical-decisions.md
Normal file
@@ -0,0 +1,316 @@
|
||||
# 프로젝트 기술 결정 사항
|
||||
|
||||
> `_index.md`에서 분리됨 (2026-02-23). 프로젝트 전반의 기술 선택 배경과 근거를 기록.
|
||||
|
||||
---
|
||||
|
||||
### `<img>` 태그 사용 — `next/image` 미사용 이유 (2026-02-10)
|
||||
|
||||
**현황**: 프로젝트 전체 `<img>` 태그 10건, `next/image` 0건
|
||||
|
||||
**결정**: `<img>` 유지, `next/image` 전환 불필요
|
||||
|
||||
**근거**:
|
||||
1. **폐쇄형 ERP 시스템** — SEO 불필요, LCP 점수 무의미
|
||||
2. **전량 외부 동적 이미지** — 백엔드 API에서 받아오는 URL (정적 내부 이미지 0건)
|
||||
3. **프린트/문서 레이아웃** — 10건 중 8건이 검사 기준서·도해 등 인쇄용. `next/image`의 `width`/`height` 강제 지정이 프린트 레이아웃을 깰 위험
|
||||
4. **blob URL 비호환** — 업로드 미리보기(blob:)는 `next/image`가 지원 안 함
|
||||
5. **설정 부담 > 이점** — `remotePatterns` 설정 + 백엔드 도메인 관리 비용이 실질 이점보다 큼
|
||||
|
||||
### 모바일 헤더 `backdrop-filter` 깜빡임 수정 (2026-02-11)
|
||||
|
||||
**현상**: 모바일(Safari/Chrome)에서 sticky 헤더가 스크롤 시 투명↔불투명 깜빡임 발생. PC 브라우저 축소로는 재현 불가, 실제 모바일 기기에서만 발생.
|
||||
|
||||
**원인 2가지**:
|
||||
1. `globals.css`에 `* { transition: all 0.2s }` — 전체 요소의 모든 CSS 속성에 전역 transition. 모바일 스크롤 리페인트 시 background/opacity가 매번 애니메이션
|
||||
2. 모바일 헤더의 `clean-glass` 클래스: `backdrop-filter: blur(8px)` + `background: rgba(255,255,255, 0.95)` 조합이 모바일 sticky 요소에서 GPU 컴포지팅 충돌
|
||||
|
||||
**수정**:
|
||||
- `globals.css`: `*` 전역 transition → `button, a, input, select, textarea, [role]` 인터랙티브 요소만, `transition: all` → `color, background-color, border-color, box-shadow` 속성만
|
||||
- 모바일 헤더: `clean-glass` (반투명+blur) → `bg-background border border-border` (불투명 배경)
|
||||
|
||||
**교훈**:
|
||||
- `transition: all`은 절대 `*`에 걸지 않기. 모바일 성능 저하 + 의도치 않은 애니메이션 발생
|
||||
- `backdrop-filter: blur()` + `sticky` 조합은 모바일 브라우저 고질적 리페인트 버그. 모바일 헤더는 불투명 배경 사용
|
||||
- 0.95 투명도는 육안 구분 불가 → 불투명 처리해도 시각적 차이 없음
|
||||
|
||||
**사용처 (9개 파일)**:
|
||||
| 파일 | 용도 | 이미지 소스 |
|
||||
|------|------|-------------|
|
||||
| `DocumentHeader.tsx` (2건) | 문서 헤더 로고 | `logo.imageUrl` (API) |
|
||||
| `ProductInspectionInputModal.tsx` | 제품검사 사진 미리보기 | blob URL |
|
||||
| `ProductInspectionDocument.tsx` | 제품검사 문서 | `data.productImage` (API) |
|
||||
| `inspection-shared.tsx` | 검사 기준서 이미지 | `standardImage` (API) |
|
||||
| `SlatInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) |
|
||||
| `ScreenInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) |
|
||||
| `BendingInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) |
|
||||
| `SlatJointBarInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) |
|
||||
| `BendingWipInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) |
|
||||
|
||||
**참고**: `next/image`가 유효한 케이스는 공개 사이트 + 정적/내부 이미지 + SEO 중요한 상황
|
||||
|
||||
### `next/dynamic` 코드 스플리팅 적용 (2026-02-10)
|
||||
|
||||
**결정**: 대형 컴포넌트 + 무거운 라이브러리에 `next/dynamic` / 동적 `import()` 적용
|
||||
|
||||
**핵심 개념 — Suspense vs dynamic()**:
|
||||
- **`Suspense` + 정적 import** → 코드가 부모와 같은 번들 청크에 포함. 유저가 안 봐도 이미 다운로드됨. UI fallback만 제공하고 **코드 분할은 안 일어남**
|
||||
- **`dynamic()`** → webpack이 별도 `.js` 청크로 분리. 컴포넌트가 실제 렌더될 때만 네트워크 요청으로 해당 청크 다운로드. **진짜 코드 분할**
|
||||
|
||||
**적용 내역**:
|
||||
|
||||
| 파일 | 대상 | 절감 |
|
||||
|------|------|------|
|
||||
| `reports/comprehensive-analysis/page.tsx` | MainDashboard (2,651줄 + recharts) | ~350KB |
|
||||
| `components/business/Dashboard.tsx` | CEODashboard | ~200KB |
|
||||
| `construction/ConstructionDashboard.tsx` | ConstructionMainDashboard | ~100KB |
|
||||
| `production/dashboard/page.tsx` | ProductionDashboard | ~100KB |
|
||||
| `lib/utils/excel-download.ts` | xlsx 라이브러리 (~400KB) | ~400KB |
|
||||
| `quotes/LocationListPanel.tsx` | xlsx 직접 import 제거 | (위와 중복) |
|
||||
|
||||
**xlsx 동적 로드 패턴**:
|
||||
```typescript
|
||||
// Before: 모든 페이지에 xlsx ~400KB 포함
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
// After: 엑셀 버튼 클릭 시에만 로드
|
||||
async function loadXLSX() {
|
||||
return await import('xlsx');
|
||||
}
|
||||
export async function downloadExcel(...) {
|
||||
const XLSX = await loadXLSX();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**총 절감**: 초기 번들에서 ~850KB 제외 (대시보드 미방문 + 엑셀 미사용 시)
|
||||
|
||||
### 테이블 가상화 (react-window) — 보류 (2026-02-10)
|
||||
|
||||
**결정**: 현시점 도입 불필요, 성능 이슈 발생 시 검토
|
||||
|
||||
**근거**:
|
||||
1. **페이지네이션 사용 중** — 리스트 페이지 대부분 서버 사이드 페이지네이션 (20~50건/페이지). 50개 `<tr>`은 브라우저가 문제없이 처리
|
||||
2. **적용 복잡도 높음** — 테이블 헤더 고정, 체크박스 선택, `rowSpan/colSpan` 병합 등 기존 기능과 충돌 가능. DataTable + IntegratedListTemplateV2 + UniversalListPage 전부 수정 필요
|
||||
3. **YAGNI** — 500건 이상 한 번에 렌더링하는 페이지가 현재 없음
|
||||
|
||||
**도입 시점**: 한 페이지에 200건+ 데이터를 페이지네이션 없이 표시해야 하는 요구가 생길 때
|
||||
|
||||
### SWR / React Query — 보류 (2026-02-10)
|
||||
|
||||
**결정**: 현시점 도입 불필요, 성능 이슈 발생 시 검토
|
||||
|
||||
**근거**:
|
||||
1. **기존 패턴 안정화 완료** — `useEffect` + Server Action 호출 패턴이 전 페이지에 일관 적용됨
|
||||
2. **캐싱 니즈 낮음** — 폐쇄형 ERP 특성상 항상 최신 데이터 필요. stale 데이터 표시는 오히려 위험
|
||||
3. **마스터데이터 캐싱 구현됨** — Zustand (`stores/masterDataStore`)로 변경 빈도 낮은 데이터는 이미 캐싱 중
|
||||
4. **도입 비용 과다** — 수십 개 페이지 `useState`+`useEffect` 패턴 전면 리팩토링 + 팀 학습 비용
|
||||
|
||||
**도입 시점**: 동일 데이터를 여러 컴포넌트에서 동시 요구하거나, 목록 ↔ 상세 이동 시 재로딩이 체감될 때
|
||||
|
||||
### 컴포넌트 레지스트리 관계도 (2026-02-12)
|
||||
|
||||
**구현**: `/dev/component-registry` 페이지에 관계도(카드형 플로우) 뷰 추가
|
||||
|
||||
**구성**:
|
||||
- `actions.ts` — `extractComponentImports()` + `buildRelationships()`로 import 관계 양방향 파싱 (imports/usedBy)
|
||||
- `ComponentRelationshipView.tsx` — 3칼럼 카드형 플로우 (사용처 → 선택 컴포넌트 → 구성요소)
|
||||
- `ComponentRegistryClient.tsx` — 목록/관계도 뷰 토글
|
||||
|
||||
**활용 규칙** (CLAUDE.md에 추가됨):
|
||||
- 새 컴포넌트 생성 전 → 목록에서 중복 검색 + 관계도에서 조합 패턴 확인
|
||||
- 기존 컴포넌트 수정 시 → usedBy로 영향 범위 파악
|
||||
|
||||
### Action 팩토리 패턴 — 신규 CRUD 적용 규칙 (2026-02-10)
|
||||
|
||||
**결정**: 기존 84개 actions.ts 전면 전환은 하지 않음. **신규 CRUD 도메인에만 팩토리 사용**
|
||||
|
||||
**현황**:
|
||||
- `src/lib/api/create-crud-service.ts` (177줄) — CRUD 보일러플레이트 자동 생성 팩토리
|
||||
- 현재 사용 중: TitleManagement, RankManagement (2개)
|
||||
- 전환 가능: 15~20개 / 전환 불가 (커스텀 로직): 50+개
|
||||
|
||||
**규칙**:
|
||||
- 신규 도메인 추가 시 단순 CRUD → `createCrudService` 사용 필수
|
||||
- 기존 actions.ts는 잘 동작하므로 무리하게 전환하지 않음
|
||||
- 커스텀 비즈니스 로직이 있는 도메인(견적, 수주, 생산 등)은 팩토리 비적합
|
||||
|
||||
**사용 예시**:
|
||||
```typescript
|
||||
import { createCrudService } from '@/lib/api/create-crud-service';
|
||||
|
||||
const service = createCrudService<ApiData, FrontendType>({
|
||||
basePath: '/api/v1/resources',
|
||||
transform: (api) => ({ id: api.id, name: api.name }),
|
||||
entityName: '리소스',
|
||||
});
|
||||
|
||||
export const getList = service.getList;
|
||||
export const getById = service.getById;
|
||||
export const create = service.create;
|
||||
export const update = service.update;
|
||||
export const remove = service.remove;
|
||||
```
|
||||
|
||||
**미전환 사유**: 84개 중 전환 가능 15~20개, 작업 2~4시간 대비 기능 변화 없음. 시간 대비 효율 낮음
|
||||
|
||||
### Server Action 공통 유틸리티 — 전체 마이그레이션 완료 (2026-02-12)
|
||||
|
||||
**결정**: `buildApiUrl()` 전체 43개 actions.ts에 적용 완료
|
||||
|
||||
**배경**:
|
||||
- 89개 actions.ts 중 43개에서 동일한 URLSearchParams 조건부 `.set()` 패턴 반복 (326+ 건)
|
||||
- 50+ 파일에서 `current_page → currentPage` 수동 변환 반복
|
||||
- `toPaginationMeta`가 `src/lib/api/types.ts`에 존재하나 import 0건
|
||||
|
||||
**생성된 유틸리티**:
|
||||
1. `src/lib/api/query-params.ts` — `buildQueryParams()`, `buildApiUrl()`: URLSearchParams 보일러플레이트 제거
|
||||
2. `src/lib/api/execute-paginated-action.ts` — `executePaginatedAction()`: 페이지네이션 조회 패턴 통합 (내부에서 `toPaginationMeta` 사용)
|
||||
|
||||
**마이그레이션 결과** (2026-02-12):
|
||||
- `new URLSearchParams` 사용: 326건 → **0건** (actions.ts 기준)
|
||||
- `const API_URL = process.env.NEXT_PUBLIC_API_URL` 선언: 43개 → **0개** (마이그레이션 대상 파일)
|
||||
- `buildApiUrl()` import: 43개 actions.ts 전체 적용
|
||||
- 3가지 API_URL 패턴 통합: 표준(`process.env`), `/api` 접미사(HR), `API_BASE` 전체경로(품질) → 모두 `buildApiUrl('/api/v1/...')` 통일
|
||||
|
||||
**`executePaginatedAction` 마이그레이션** (2026-02-12):
|
||||
- 14개 actions.ts에서 페이지네이션 목록 조회 함수를 `executePaginatedAction`으로 전환
|
||||
- Wave A (accounting 9개): BillManagement, DepositManagement, SalesManagement, PurchaseManagement, WithdrawalManagement, VendorLedger, CardTransactionInquiry, BankTransactionInquiry, ExpectedExpenseManagement
|
||||
- Wave B (5개): PaymentHistoryManagement, StockStatus, ReceivingManagement, ShipmentManagement, quotes
|
||||
- 제외 5개: AccountManagement(`meta` 필드명), orders(`data.items` 중첩), VacationManagement, EmployeeManagement, construction/order-management (별도 구조)
|
||||
- 순 감소: ~220줄 (14파일 × ~20줄 제거, ~28줄 추가)
|
||||
- 제거된 보일러플레이트: `DEFAULT_PAGINATION`, `FrontendPagination`/`PaginationMeta` 로컬 인터페이스, `PaginatedApiResponse` import, 수동 transform+pagination 조립
|
||||
- **화면 검수 완료** (4개 페이지): Bills, StockStatus, Quotes, Shipments — 전체 PASS
|
||||
- **버그 발견/수정**: `quotes/actions.ts`에서 `export type { PaginationMeta }` re-export가 Turbopack 런타임 에러 유발 (`tsc`로 미감지) → re-export 제거, 컴포넌트에서 `@/lib/api/types` 직접 import로 변경
|
||||
|
||||
### `'use server'` 파일 타입 export 제한 (2026-02-12)
|
||||
|
||||
**발견 배경**: `executePaginatedAction` 마이그레이션 화면 검수 중 견적관리 페이지 빌드 에러
|
||||
|
||||
**제한 사항**:
|
||||
- `'use server'` 파일에서는 **async 함수만 export 가능** (Next.js Turbopack 제한)
|
||||
- `export type { X } from '...'` (re-export) → **런타임 에러 발생**
|
||||
- `export interface X { ... }` / `export type X = ...` (인라인 정의) → **문제 없음** (컴파일 시 제거)
|
||||
- `tsc --noEmit`으로는 감지 불가 — Next.js 전용 규칙이므로 실제 페이지 접속(Turbopack)에서만 발생
|
||||
|
||||
**현재 상태**: 전체 81개 `'use server'` 파일 점검 완료, re-export 패턴 0건 (수정된 1건 포함)
|
||||
|
||||
**buildApiUrl 마이그레이션 전략**:
|
||||
- Wave A: 1건짜리 단순 파일 20개
|
||||
- Wave B: 2건짜리 파일 12개 (quotes, WorkOrders, orders 등 대형 파일 포함)
|
||||
- Wave C: 3건 이상 파일 12개 (VendorLedger 5건, ReceivingManagement 5건, ProcessManagement 19건 URL 등)
|
||||
|
||||
**효과**:
|
||||
- 페이지네이션 조회 코드: ~20줄 → ~5줄
|
||||
- `DEFAULT_PAGINATION` 중앙화 (`execute-paginated-action.ts` 내부)
|
||||
- `toPaginationMeta` 자동 활용 (직접 import 불필요)
|
||||
- URL 빌딩 패턴 완전 일관화 (undefined/null/'' 자동 필터링, boolean/number 자동 변환)
|
||||
|
||||
### KST 안전 날짜 유틸리티 — `toISOString` 사용 금지 (2026-02-19)
|
||||
|
||||
**현황**: `new Date().toISOString().split('T')[0]` — 15개 파일 26곳에서 사용 중이었음
|
||||
|
||||
**문제**: `toISOString()`은 **UTC 기준**으로 변환. 한국(KST, UTC+9)에서 오전 9시 이전에 실행하면 **전날 날짜** 반환
|
||||
```
|
||||
// 2026-02-19 08:30 KST → UTC는 2026-02-18 23:30
|
||||
new Date().toISOString().split('T')[0] // "2026-02-18" ← 잘못됨
|
||||
```
|
||||
|
||||
**결정**: KST 안전 유틸리티 함수로 전량 교체, 직접 `toISOString` 사용 금지
|
||||
|
||||
**유틸리티** (`src/lib/utils/date.ts`):
|
||||
| 함수 | 용도 | 대체 대상 |
|
||||
|------|------|-----------|
|
||||
| `getTodayString()` | 오늘 날짜 문자열 | `new Date().toISOString().split('T')[0]` |
|
||||
| `getLocalDateString(date)` | 임의 Date 객체 문자열 | `someDate.toISOString().split('T')[0]` |
|
||||
|
||||
**사용 규칙**:
|
||||
```typescript
|
||||
// 올바른 패턴
|
||||
import { getTodayString, getLocalDateString } from '@/lib/utils/date';
|
||||
const today = getTodayString(); // "2026-02-19"
|
||||
const thirtyDaysAgo = getLocalDateString(pastDate); // "2026-01-20"
|
||||
|
||||
// 금지 패턴
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
```
|
||||
|
||||
**현재 상태**: `src/` 내 `toISOString().split` 사용 0건 (date.ts 내 구현부 제외)
|
||||
|
||||
### 달력/스케줄 공통 리소스 — 작업 전 필수 확인 (2026-02-23)
|
||||
|
||||
달력·일정·날짜 관련 작업 시 아래 공통 리소스를 **반드시 확인**하고 사용할 것.
|
||||
|
||||
**날짜 유틸리티** (`src/lib/utils/date.ts`):
|
||||
| 함수 | 용도 |
|
||||
|------|------|
|
||||
| `getLocalDateString(date)` | Date → `'YYYY-MM-DD'` (KST 안전) |
|
||||
| `getTodayString()` | 오늘 날짜 문자열 |
|
||||
| `formatDate(dateStr)` | 표시용 날짜 포맷 (null → `'-'`) |
|
||||
| `formatDateForInput(dateStr)` | input용 `YYYY-MM-DD` 변환 |
|
||||
| `formatDateRange(start, end)` | `'시작 ~ 종료'` 포맷 |
|
||||
| `getDateAfterDays(n)` | N일 후 날짜 |
|
||||
|
||||
**달력 일정 스토어** (`src/stores/useCalendarScheduleStore.ts`):
|
||||
- 달력관리(CalendarManagement)에서 등록한 공휴일/세무일정/회사일정을 프로젝트 전체에 공유
|
||||
- `fetchSchedules(year)` — 연도별 캐시 조회 (API 호출)
|
||||
- `setSchedulesForYear(year, data)` — 이미 가져온 데이터 직접 설정
|
||||
- `invalidateYear(year)` — 캐시 무효화 (등록/수정/삭제 후)
|
||||
- **현재 상태**: 백엔드 API 미구현 → 호출부 주석 처리 (TODO 검색)
|
||||
|
||||
**달력 이벤트 유틸** (`src/constants/calendarEvents.ts`):
|
||||
- `isHoliday(date)`, `isTaxDeadline(date)`, `getHolidayName(date)` 등
|
||||
- 스토어 우선 → 하드코딩 폴백(2026년) 패턴
|
||||
- 새 연도 폴백 데이터 필요 시 이 파일에 `HOLIDAYS_YYYY`, `TAX_DEADLINES_YYYY` 추가
|
||||
|
||||
**ScheduleCalendar 공통 컴포넌트** (`src/components/common/ScheduleCalendar/`):
|
||||
- `hideNavigation` prop으로 헤더 숨김 가능 (연간 달력 등 상위 네비게이션 사용 시)
|
||||
- `availableViews={[]}` 으로 뷰 전환 버튼 숨김
|
||||
|
||||
**규칙**:
|
||||
- `Date → string` 변환 시 `getLocalDateString()` 필수 (`toISOString().split('T')[0]` 금지)
|
||||
- 공휴일/세무일 판별 시 `calendarEvents.ts` 유틸 함수 사용
|
||||
- 달력 데이터 공유 시 zustand 스토어 경유 (컴포넌트 간 직접 전달 금지)
|
||||
|
||||
### `useDateRange` 훅 — 날짜 필터 보일러플레이트 제거 (2026-02-19)
|
||||
|
||||
**현황**: 20+ 리스트 페이지에서 `useState('2025-01-01')` / `useState('2025-12-31')` 하드코딩
|
||||
|
||||
**문제**: 연도가 바뀌면 수동으로 모든 파일 수정 필요 (2025→2026 전환 시 데이터 미표시 버그 발생)
|
||||
|
||||
**결정**: `useDateRange` 훅으로 동적 날짜 범위 자동 계산
|
||||
|
||||
**훅** (`src/hooks/useDateRange.ts`):
|
||||
```typescript
|
||||
import { useDateRange } from '@/hooks';
|
||||
|
||||
// 프리셋
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear'); // 2026-01-01 ~ 2026-12-31
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentMonth'); // 2026-02-01 ~ 2026-02-28
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('today'); // 2026-02-19 ~ 2026-02-19
|
||||
```
|
||||
|
||||
**적용 규칙**:
|
||||
- 리스트 페이지 날짜 필터 → `useDateRange` 필수 사용
|
||||
- 연간 조회 → `'currentYear'`, 월간 조회 → `'currentMonth'`
|
||||
- `useState('YYYY-MM-DD')` 하드코딩 금지
|
||||
|
||||
**현재 상태**: `useState('2025` 패턴 0건 (전량 `useDateRange`로 전환 완료)
|
||||
|
||||
### Zod 스키마 검증 — 신규 폼 적용 규칙 (2026-02-11)
|
||||
|
||||
**결정**: 기존 폼은 건드리지 않음. **신규 폼에만 Zod + zodResolver 적용**
|
||||
|
||||
**설치 상태**: `zod@^4.1.12`, `@hookform/resolvers@^5.2.2` — 이미 설치됨
|
||||
|
||||
**효과**:
|
||||
1. 스키마 하나로 **타입 추론 + 런타임 검증** 동시 해결 (`z.infer<typeof schema>`)
|
||||
2. 별도 `interface` 중복 정의 불필요
|
||||
3. 신규 코드에서 `as` 캐스트 자연 감소 (D-2 개선 효과)
|
||||
|
||||
**규칙**:
|
||||
- 신규 폼 → `zodResolver(schema)` 사용 필수 (CLAUDE.md에 패턴 명시)
|
||||
- 기존 `rules={{ required: true }}` 패턴 폼 → 마이그레이션 불필요
|
||||
- 단순 1~2 필드 인라인 폼 → Zod 불필요 (오버엔지니어링)
|
||||
|
||||
**미적용 사유**: 기존 폼 수십 개를 전면 전환하는 비용 >> 이득. 신규 코드에서 점진적 확산
|
||||
260
claudedocs/architecture/[REF] template-migration-status.md
Normal file
260
claudedocs/architecture/[REF] template-migration-status.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# 템플릿 마이그레이션 현황
|
||||
|
||||
> 작성일: 2025-01-20
|
||||
> 목적: IntegratedListTemplate / IntegratedDetailTemplate 적용 현황 파악
|
||||
|
||||
---
|
||||
|
||||
## 📊 전체 통계
|
||||
|
||||
| 구분 | 수량 |
|
||||
|------|------|
|
||||
| 전체 Protected 페이지 | 203개 |
|
||||
| IntegratedListTemplate 사용 | 48개 |
|
||||
| IntegratedDetailTemplate 사용 | 57개 |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 마이그레이션 완료
|
||||
|
||||
### 리스트 페이지 (IntegratedListTemplateV2)
|
||||
대부분의 리스트 페이지가 IntegratedListTemplateV2로 마이그레이션 완료.
|
||||
- 필터, 테이블, 페이지네이션, 헤더 버튼 공통화 적용
|
||||
|
||||
### 상세/수정/등록 페이지 (IntegratedDetailTemplate)
|
||||
|
||||
#### App 페이지 (17개)
|
||||
```
|
||||
src/app/[locale]/(protected)/settings/popup-management/new/page.tsx
|
||||
src/app/[locale]/(protected)/settings/popup-management/[id]/page.tsx
|
||||
src/app/[locale]/(protected)/settings/accounts/new/page.tsx
|
||||
src/app/[locale]/(protected)/settings/accounts/[id]/page.tsx
|
||||
src/app/[locale]/(protected)/sales/client-management-sales-admin/new/page.tsx
|
||||
src/app/[locale]/(protected)/sales/client-management-sales-admin/[id]/page.tsx
|
||||
src/app/[locale]/(protected)/sales/quote-management/test/[id]/page.tsx
|
||||
src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx
|
||||
src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx
|
||||
src/app/[locale]/(protected)/board/board-management/new/page.tsx
|
||||
src/app/[locale]/(protected)/board/board-management/[id]/page.tsx
|
||||
src/app/[locale]/(protected)/master-data/process-management/new/page.tsx
|
||||
src/app/[locale]/(protected)/master-data/process-management/[id]/page.tsx
|
||||
src/app/[locale]/(protected)/hr/card-management/new/page.tsx
|
||||
src/app/[locale]/(protected)/hr/card-management/[id]/edit/page.tsx
|
||||
src/app/[locale]/(protected)/hr/card-management/[id]/page.tsx
|
||||
src/app/[locale]/(protected)/construction/order/base-info/labor/[id]/page.tsx
|
||||
```
|
||||
|
||||
#### 컴포넌트 (주요 40개)
|
||||
```
|
||||
# 회계
|
||||
src/components/accounting/BillManagement/BillDetail.tsx
|
||||
src/components/accounting/SalesManagement/SalesDetail.tsx
|
||||
src/components/accounting/PurchaseManagement/PurchaseDetail.tsx
|
||||
src/components/accounting/VendorLedger/VendorLedgerDetail.tsx
|
||||
src/components/accounting/VendorManagement/VendorDetail.tsx
|
||||
src/components/accounting/BadDebtCollection/BadDebtDetail.tsx
|
||||
src/components/accounting/WithdrawalManagement/WithdrawalDetailClientV2.tsx
|
||||
src/components/accounting/DepositManagement/DepositDetailClientV2.tsx
|
||||
|
||||
# 영업/고객
|
||||
src/components/clients/ClientDetailClientV2.tsx
|
||||
src/components/quotes/QuoteRegistrationV2.tsx
|
||||
src/components/orders/OrderSalesDetailView.tsx
|
||||
src/components/orders/OrderSalesDetailEdit.tsx
|
||||
|
||||
# 설정
|
||||
src/components/settings/PopupManagement/PopupDetailClientV2.tsx
|
||||
src/components/settings/PermissionManagement/PermissionDetail.tsx
|
||||
|
||||
# 건설/프로젝트
|
||||
src/components/business/construction/contract/ContractDetailForm.tsx
|
||||
src/components/business/construction/site-briefings/SiteBriefingForm.tsx
|
||||
src/components/business/construction/order-management/OrderDetailForm.tsx
|
||||
src/components/business/construction/handover-report/HandoverReportDetailForm.tsx
|
||||
src/components/business/construction/item-management/ItemDetailClient.tsx
|
||||
src/components/business/construction/estimates/EstimateDetailForm.tsx
|
||||
src/components/business/construction/management/ConstructionDetailClient.tsx
|
||||
src/components/business/construction/site-management/SiteDetailForm.tsx
|
||||
src/components/business/construction/partners/PartnerForm.tsx
|
||||
src/components/business/construction/structure-review/StructureReviewDetailForm.tsx
|
||||
src/components/business/construction/issue-management/IssueDetailForm.tsx
|
||||
src/components/business/construction/bidding/BiddingDetailForm.tsx
|
||||
src/components/business/construction/pricing-management/PricingDetailClientV2.tsx
|
||||
src/components/business/construction/labor-management/LaborDetailClientV2.tsx
|
||||
src/components/business/construction/progress-billing/ProgressBillingDetailForm.tsx
|
||||
|
||||
# 고객센터
|
||||
src/components/customer-center/NoticeManagement/NoticeDetail.tsx
|
||||
src/components/customer-center/InquiryManagement/InquiryDetail.tsx
|
||||
src/components/customer-center/EventManagement/EventDetail.tsx
|
||||
|
||||
# HR
|
||||
src/components/hr/EmployeeManagement/EmployeeDetail.tsx
|
||||
|
||||
# 생산/물류
|
||||
src/components/production/WorkOrders/WorkOrderDetail.tsx
|
||||
src/components/outbound/ShipmentManagement/ShipmentDetail.tsx
|
||||
src/components/material/ReceivingManagement/ReceivingDetail.tsx
|
||||
src/components/material/StockStatus/StockStatusDetail.tsx
|
||||
|
||||
# 품질
|
||||
src/components/quality/InspectionManagement/InspectionDetail.tsx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ❌ 마이그레이션 미완료
|
||||
|
||||
### App 페이지 (PageLayout 직접 사용)
|
||||
|
||||
| 경로 | 유형 | 비고 |
|
||||
|------|------|------|
|
||||
| `sales/order-management-sales/production-orders/[id]/page.tsx` | 상세 | 생산지시 상세 |
|
||||
| `sales/order-management-sales/[id]/production-order/page.tsx` | 상세 | 생산지시 |
|
||||
| `boards/[boardCode]/create/page.tsx` | 등록 | 게시판 글쓰기 |
|
||||
| `boards/[boardCode]/[postId]/edit/page.tsx` | 수정 | 게시판 글수정 |
|
||||
| `boards/[boardCode]/[postId]/page.tsx` | 상세 | 게시판 글상세 |
|
||||
| `test/popup/page.tsx` | 테스트 | 테스트 페이지 |
|
||||
| `dev/editable-table/page.tsx` | 개발 | 개발용 페이지 |
|
||||
|
||||
### 컴포넌트 (PageLayout 직접 사용)
|
||||
|
||||
#### 회계
|
||||
```
|
||||
src/components/accounting/DailyReport/index.tsx # 일일보고서 (특수 레이아웃)
|
||||
src/components/accounting/ReceivablesStatus/index.tsx # 미수금현황 (특수 레이아웃)
|
||||
src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx # V2로 대체됨
|
||||
src/components/accounting/DepositManagement/DepositDetail.tsx # V2로 대체됨
|
||||
```
|
||||
|
||||
#### 설정
|
||||
```
|
||||
src/components/settings/CompanyInfoManagement/index.tsx # 회사정보 (설정 페이지)
|
||||
src/components/settings/RankManagement/index.tsx # 직급관리 (설정 페이지)
|
||||
src/components/settings/LeavePolicyManagement/index.tsx # 휴가정책 (설정 페이지)
|
||||
src/components/settings/AccountInfoManagement/index.tsx # 계정정보 (설정 페이지)
|
||||
src/components/settings/NotificationSettings/index.tsx # 알림설정 (설정 페이지)
|
||||
src/components/settings/TitleManagement/index.tsx # 직책관리 (설정 페이지)
|
||||
src/components/settings/WorkScheduleManagement/index.tsx # 근무일정 (설정 페이지)
|
||||
src/components/settings/AttendanceSettingsManagement/index.tsx # 근태설정 (설정 페이지)
|
||||
src/components/settings/PopupManagement/PopupForm.tsx # V2로 대체됨
|
||||
src/components/settings/PopupManagement/PopupDetail.tsx # V2로 대체됨
|
||||
src/components/settings/AccountManagement/AccountDetail.tsx # V2로 대체됨
|
||||
src/components/settings/PermissionManagement/PermissionDetailClient.tsx # 레거시
|
||||
src/components/settings/SubscriptionManagement/SubscriptionManagement.tsx # 구독관리
|
||||
src/components/settings/SubscriptionManagement/SubscriptionClient.tsx
|
||||
```
|
||||
|
||||
#### 건설/프로젝트
|
||||
```
|
||||
src/components/business/construction/management/ProjectListClient.tsx # 리스트 (별도)
|
||||
src/components/business/construction/management/ProjectDetailClient.tsx # 레거시
|
||||
src/components/business/construction/category-management/index.tsx # 카테고리 (특수)
|
||||
src/components/business/construction/pricing-management/PricingDetailClient.tsx # V2로 대체됨
|
||||
src/components/business/construction/labor-management/LaborDetailClient.tsx # V2로 대체됨
|
||||
```
|
||||
|
||||
#### 게시판
|
||||
```
|
||||
src/components/board/BoardManagement/BoardForm.tsx # V2로 대체됨
|
||||
src/components/board/BoardManagement/BoardDetail.tsx # V2로 대체됨
|
||||
src/components/board/BoardDetail/index.tsx # 동적 게시판 상세
|
||||
src/components/board/BoardForm/index.tsx # 동적 게시판 폼
|
||||
```
|
||||
|
||||
#### HR
|
||||
```
|
||||
src/components/hr/DepartmentManagement/index.tsx # 부서관리 (트리 구조)
|
||||
src/components/hr/EmployeeManagement/EmployeeForm.tsx # 직원등록 폼
|
||||
src/components/hr/EmployeeManagement/CSVUploadPage.tsx # CSV 업로드 (특수)
|
||||
```
|
||||
|
||||
#### 생산
|
||||
```
|
||||
src/components/production/ProductionDashboard/index.tsx # 대시보드 (제외)
|
||||
src/components/production/WorkerScreen/index.tsx # 작업자화면 (특수 UI)
|
||||
src/components/production/WorkOrders/WorkOrderCreate.tsx # 작업지시 등록
|
||||
src/components/production/WorkOrders/WorkOrderEdit.tsx # 작업지시 수정
|
||||
```
|
||||
|
||||
#### 고객센터
|
||||
```
|
||||
src/components/customer-center/InquiryManagement/InquiryForm.tsx # 문의등록
|
||||
src/components/customer-center/FAQManagement/FAQList.tsx # FAQ 리스트
|
||||
```
|
||||
|
||||
#### 기타
|
||||
```
|
||||
src/components/clients/ClientDetail.tsx # V2로 대체됨
|
||||
src/components/process-management/ProcessForm.tsx # 공정등록
|
||||
src/components/process-management/ProcessDetail.tsx # V2로 대체됨
|
||||
src/components/outbound/ShipmentManagement/ShipmentEdit.tsx # 출고수정
|
||||
src/components/outbound/ShipmentManagement/ShipmentCreate.tsx # 출고등록
|
||||
src/components/items/ItemMasterDataManagement.tsx # 품목마스터 (특수)
|
||||
src/components/material/ReceivingManagement/InspectionCreate.tsx # 검수등록
|
||||
src/components/quality/InspectionManagement/InspectionCreate.tsx # 품질검사등록
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚫 마이그레이션 제외 대상
|
||||
|
||||
### 대시보드/특수 페이지
|
||||
```
|
||||
src/app/[locale]/(protected)/dashboard/page.tsx # CEO 대시보드
|
||||
src/app/[locale]/(protected)/production/dashboard/page.tsx # 생산 대시보드
|
||||
src/app/[locale]/(protected)/reports/comprehensive-analysis/page.tsx # 종합분석
|
||||
src/components/business/CEODashboard/CEODashboard.tsx # CEO 대시보드
|
||||
```
|
||||
|
||||
### 레거시 파일 (_legacy 폴더)
|
||||
```
|
||||
src/components/settings/AccountManagement/_legacy/AccountDetail.tsx
|
||||
src/components/hr/CardManagement/_legacy/CardDetail.tsx
|
||||
src/components/hr/CardManagement/_legacy/CardForm.tsx
|
||||
```
|
||||
|
||||
### 테스트/개발용
|
||||
```
|
||||
src/app/[locale]/(protected)/test/popup/page.tsx
|
||||
src/app/[locale]/(protected)/dev/editable-table/page.tsx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 마이그레이션 우선순위 권장
|
||||
|
||||
### 높음 (실사용 페이지)
|
||||
1. `boards/[boardCode]/*` - 동적 게시판 페이지들
|
||||
2. `production/WorkOrders/WorkOrderCreate.tsx` - 작업지시 등록
|
||||
3. `production/WorkOrders/WorkOrderEdit.tsx` - 작업지시 수정
|
||||
4. `outbound/ShipmentManagement/ShipmentCreate.tsx` - 출고 등록
|
||||
5. `outbound/ShipmentManagement/ShipmentEdit.tsx` - 출고 수정
|
||||
|
||||
### 중간 (설정 페이지 - 템플릿 적용 검토 필요)
|
||||
- `settings/` 하위 관리 페이지들 (트리/특수 레이아웃 많음)
|
||||
|
||||
### 낮음 (V2 대체 완료)
|
||||
- V2 파일이 있는 레거시 컴포넌트들 (삭제 검토)
|
||||
|
||||
### 제외
|
||||
- 대시보드, 특수 UI, 테스트/개발 페이지
|
||||
|
||||
---
|
||||
|
||||
## 🔧 템플릿 수정 시 일괄 적용 범위
|
||||
|
||||
템플릿 파일 수정 시 아래 파일들에 자동 적용:
|
||||
|
||||
| 템플릿 | 영향 파일 수 |
|
||||
|--------|-------------|
|
||||
| `IntegratedListTemplateV2` | 48개 |
|
||||
| `IntegratedDetailTemplate` | 57개 |
|
||||
| **합계** | **105개** |
|
||||
|
||||
수정 가능 요소:
|
||||
- 타이틀 위치/스타일
|
||||
- 버튼 배치/디자인
|
||||
- 입력필드 공통 스타일
|
||||
- 레이아웃 구조
|
||||
- 반응형 처리
|
||||
@@ -0,0 +1,606 @@
|
||||
# Research: Next.js / React ERP & Admin Panel Architecture Patterns (2025-2026)
|
||||
|
||||
**Date**: 2026-02-11
|
||||
**Purpose**: Compare SAM ERP's current architecture against proven open-source patterns
|
||||
**Confidence**: High (0.85) - Based on 6 major open-source projects and established methodologies
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
After investigating 6 major open-source admin/ERP frameworks and 3 architectural methodologies, the dominant pattern emerging in 2025-2026 is a **hybrid approach**: domain/feature-based folder organization combined with headless CRUD hooks and a provider-based API abstraction layer. Pure Atomic Design is losing ground to Feature-Sliced Design (FSD) for application-level organization, though Atomic Design remains useful for the shared UI component layer.
|
||||
|
||||
### Key Findings
|
||||
|
||||
1. **Resource-based CRUD abstraction** (react-admin, Refine) is the most proven pattern for 50+ page admin apps
|
||||
2. **Feature/domain-based folder structure** is winning over layer-based (atoms/molecules/organisms) for application code
|
||||
3. **Provider pattern** (dataProvider, authProvider) decouples UI from API more effectively than scattered Server Actions
|
||||
4. **Config-driven UI generation** (Payload CMS) reduces code duplication for similar pages
|
||||
5. **Headless hooks** (useListController, useTable, useForm) separate business logic from UI completely
|
||||
|
||||
---
|
||||
|
||||
## 1. Project-by-Project Architecture Analysis
|
||||
|
||||
### 1.1 React-Admin (marmelab) -- 25K+ GitHub Stars
|
||||
|
||||
**Architecture**: Resource-based SPA with Provider pattern
|
||||
|
||||
**Key Concepts**:
|
||||
- **Resources**: The core abstraction. Each entity (posts, users, orders) is a "resource" with CRUD views
|
||||
- **Providers**: Adapter layer between UI and backend
|
||||
- `dataProvider` - abstracts all API calls (getList, getOne, create, update, delete)
|
||||
- `authProvider` - handles authentication flow
|
||||
- `i18nProvider` - internationalization
|
||||
- **Headless Core**: `ra-core` package contains all hooks, zero UI dependency
|
||||
- **Controller Hooks**: `useListController`, `useEditController`, `useCreateController`, `useShowController`
|
||||
|
||||
**Folder Pattern**:
|
||||
```
|
||||
src/
|
||||
resources/
|
||||
posts/
|
||||
PostList.tsx # <List> view
|
||||
PostEdit.tsx # <Edit> view
|
||||
PostCreate.tsx # <Create> view
|
||||
PostShow.tsx # <Show> view
|
||||
users/
|
||||
UserList.tsx
|
||||
UserEdit.tsx
|
||||
providers/
|
||||
dataProvider.ts # API abstraction
|
||||
authProvider.ts # Auth abstraction
|
||||
App.tsx # Resource registration
|
||||
```
|
||||
|
||||
**CRUD Registration Pattern**:
|
||||
```tsx
|
||||
<Admin dataProvider={dataProvider} authProvider={authProvider}>
|
||||
<Resource name="posts" list={PostList} edit={PostEdit} create={PostCreate} />
|
||||
<Resource name="users" list={UserList} edit={UserEdit} />
|
||||
</Admin>
|
||||
```
|
||||
|
||||
**SAM Comparison**:
|
||||
| Aspect | react-admin | SAM ERP |
|
||||
|--------|-------------|---------|
|
||||
| API Layer | Centralized dataProvider | 89 scattered actions.ts files |
|
||||
| CRUD Views | Resource-based registration | Manual page creation per domain |
|
||||
| State | React Query (built-in) | Zustand + manual fetching |
|
||||
| Form | react-hook-form (built-in) | Mixed (migrating to RHF+Zod) |
|
||||
|
||||
**Sources**:
|
||||
- [Architecture Docs](https://marmelab.com/react-admin/Architecture.html)
|
||||
- [Resource Component](https://marmelab.com/react-admin/Resource.html)
|
||||
- [CRUD Pages](https://marmelab.com/react-admin/CRUD.html)
|
||||
- [GitHub](https://github.com/marmelab/react-admin)
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Refine -- 30K+ GitHub Stars
|
||||
|
||||
**Architecture**: Headless meta-framework with resource-based CRUD
|
||||
|
||||
**Key Concepts**:
|
||||
- **Headless by design**: Zero UI opinion, works with Ant Design, Material UI, Shadcn, or custom
|
||||
- **Data Provider Interface**: Standardized CRUD methods (getList, getOne, create, update, deleteOne)
|
||||
- **Resource Hooks**: `useTable`, `useForm`, `useShow`, `useSelect` -- all headless
|
||||
- **Inferencer**: Auto-generates CRUD pages from API schema
|
||||
|
||||
**Data Provider Interface**:
|
||||
```typescript
|
||||
const dataProvider = {
|
||||
getList: ({ resource, pagination, sorters, filters }) => Promise,
|
||||
getOne: ({ resource, id }) => Promise,
|
||||
create: ({ resource, variables }) => Promise,
|
||||
update: ({ resource, id, variables }) => Promise,
|
||||
deleteOne: ({ resource, id }) => Promise,
|
||||
getMany: ({ resource, ids }) => Promise,
|
||||
custom: ({ url, method, payload }) => Promise,
|
||||
};
|
||||
```
|
||||
|
||||
**Headless Hook Pattern**:
|
||||
```tsx
|
||||
// useTable returns data + controls, you handle UI
|
||||
const { tableProps, sorters, filters } = useTable({ resource: "products" });
|
||||
|
||||
// useForm returns form state + submit, you handle UI
|
||||
const { formProps, saveButtonProps } = useForm({ resource: "products", action: "create" });
|
||||
```
|
||||
|
||||
**SAM Comparison**:
|
||||
| Aspect | Refine | SAM ERP |
|
||||
|--------|--------|---------|
|
||||
| API Abstraction | Single dataProvider | Per-domain actions.ts |
|
||||
| List Page | useTable hook | UniversalListPage template |
|
||||
| Form | useForm hook (headless) | Manual per-page forms |
|
||||
| Code Generation | Inferencer auto-gen | Manual creation |
|
||||
|
||||
**Sources**:
|
||||
- [Data Provider Docs](https://refine.dev/docs/data/data-provider/)
|
||||
- [useTable Hook](https://refine.dev/docs/data/hooks/use-table/)
|
||||
- [GitHub](https://github.com/refinedev/refine)
|
||||
|
||||
---
|
||||
|
||||
### 1.3 Payload CMS 3.0 -- 30K+ GitHub Stars
|
||||
|
||||
**Architecture**: Config-driven, Next.js-native with auto-generated admin UI
|
||||
|
||||
**Key Concepts**:
|
||||
- **Collection Config**: Define schema once, get admin UI + API + types automatically
|
||||
- **Field System**: Rich field types auto-generate corresponding UI components
|
||||
- **Hooks**: beforeChange, afterRead, beforeValidate at collection and field level
|
||||
- **Access Control**: Document-level and field-level permissions in config
|
||||
- **Next.js Native**: Installs directly into /app folder, uses Server Components
|
||||
|
||||
**Config-Driven Pattern**:
|
||||
```typescript
|
||||
// collections/Products.ts
|
||||
export const Products: CollectionConfig = {
|
||||
slug: 'products',
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
defaultColumns: ['name', 'price', 'status'],
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
create: isAdmin,
|
||||
update: isAdminOrSelf,
|
||||
},
|
||||
hooks: {
|
||||
beforeChange: [calculateTotal],
|
||||
afterRead: [formatCurrency],
|
||||
},
|
||||
fields: [
|
||||
{ name: 'name', type: 'text', required: true },
|
||||
{ name: 'price', type: 'number', min: 0 },
|
||||
{ name: 'status', type: 'select', options: ['draft', 'published'] },
|
||||
{ name: 'category', type: 'relationship', relationTo: 'categories' },
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
**SAM Comparison**:
|
||||
| Aspect | Payload CMS | SAM ERP |
|
||||
|--------|-------------|---------|
|
||||
| Page Generation | Auto from config | Manual per page |
|
||||
| Field Definitions | Centralized schema | Inline JSX per form |
|
||||
| Access Control | Config-based per field | Manual per component |
|
||||
| Type Safety | Auto-generated from schema | Manual interface definitions |
|
||||
|
||||
**Sources**:
|
||||
- [Collection Configs](https://payloadcms.com/docs/configuration/collections)
|
||||
- [Fields Overview](https://payloadcms.com/docs/fields/overview)
|
||||
- [Collection Hooks](https://payloadcms.com/docs/hooks/collections)
|
||||
- [GitHub](https://github.com/payloadcms/payload)
|
||||
|
||||
---
|
||||
|
||||
### 1.4 Medusa Admin v2 -- 26K+ GitHub Stars
|
||||
|
||||
**Architecture**: Domain-based routes with widget injection system
|
||||
|
||||
**Key Concepts**:
|
||||
- **Domain Routes**: Routes organized by business domain (products, orders, customers)
|
||||
- **Widget System**: Inject custom React components into predetermined zones
|
||||
- **UI Routes**: File-based routing under src/admin/routes/
|
||||
- **Hook-based data fetching**: Domain-specific hooks for API integration
|
||||
- **Monorepo**: UI library (@medusajs/ui) separate from admin logic
|
||||
|
||||
**Folder Structure**:
|
||||
```
|
||||
packages/admin/dashboard/src/
|
||||
routes/
|
||||
products/
|
||||
product-list/
|
||||
components/
|
||||
hooks/
|
||||
page.tsx
|
||||
product-detail/
|
||||
components/
|
||||
hooks/
|
||||
page.tsx
|
||||
orders/
|
||||
order-list/
|
||||
order-detail/
|
||||
customers/
|
||||
hooks/ # Shared hooks
|
||||
components/ # Shared components
|
||||
lib/ # Utilities
|
||||
```
|
||||
|
||||
**SAM Comparison**:
|
||||
| Aspect | Medusa Admin | SAM ERP |
|
||||
|--------|-------------|---------|
|
||||
| Route Organization | Domain > Action > Components | Domain > page.tsx + actions.ts |
|
||||
| Shared Components | Separate UI package | organisms/molecules/atoms |
|
||||
| Hooks | Per-route + shared | Global + inline |
|
||||
| Extensibility | Widget injection zones | N/A |
|
||||
|
||||
**Sources**:
|
||||
- [Admin UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes)
|
||||
- [Admin Development](https://docs.medusajs.com/learn/fundamentals/admin)
|
||||
- [GitHub](https://github.com/medusajs/medusa)
|
||||
|
||||
---
|
||||
|
||||
### 1.5 AdminJS
|
||||
|
||||
**Architecture**: Auto-generated admin from resource configuration
|
||||
|
||||
**Key Concepts**:
|
||||
- **Resource Registration**: Register database models, get admin UI automatically
|
||||
- **Component Customization**: Override via ComponentLoader
|
||||
- **Dashboard Customization**: Custom React components for dashboard
|
||||
|
||||
**SAM Relevance**: Lower -- AdminJS is more backend-driven (Node.js ORM-based) and less applicable to a frontend-heavy ERP.
|
||||
|
||||
**Sources**:
|
||||
- [AdminJS Documentation](https://adminjs.co/)
|
||||
- [GitHub](https://github.com/SoftwareBrothers/adminjs)
|
||||
|
||||
---
|
||||
|
||||
### 1.6 Hoppscotch
|
||||
|
||||
**Architecture**: Monorepo with shared-library pattern
|
||||
|
||||
**Key Concepts**:
|
||||
- **@hoppscotch/common**: 90% of UI and business logic in shared package
|
||||
- **@hoppscotch/data**: Type safety across all layers
|
||||
- **Platform-specific code**: Thin wrapper handling native capabilities
|
||||
|
||||
**SAM Relevance**: The shared-library-as-core pattern is interesting for large codebases where most logic is platform-agnostic.
|
||||
|
||||
**Sources**:
|
||||
- [DeepWiki Analysis](https://deepwiki.com/hoppscotch/hoppscotch)
|
||||
|
||||
---
|
||||
|
||||
## 2. Architectural Methodologies Comparison
|
||||
|
||||
### 2.1 Feature-Sliced Design (FSD) -- Rising Standard
|
||||
|
||||
**7-Layer Architecture**:
|
||||
```
|
||||
app/ # App initialization, providers, routing
|
||||
processes/ # Complex cross-page business flows (deprecated in latest)
|
||||
pages/ # Full page compositions
|
||||
widgets/ # Self-contained UI blocks with business logic
|
||||
features/ # User-facing actions (login, add-to-cart)
|
||||
entities/ # Business entities (user, product, order)
|
||||
shared/ # Reusable utilities, UI kit, configs
|
||||
```
|
||||
|
||||
**Key Rules**:
|
||||
- Layers can ONLY import from layers below them
|
||||
- Each layer divided into **slices** (domain groupings)
|
||||
- Each slice divided into **segments** (ui/, model/, api/, lib/, config/)
|
||||
|
||||
**FSD Applied to ERP**:
|
||||
```
|
||||
src/
|
||||
app/ # App shell, providers
|
||||
pages/
|
||||
quality-qms/ # QMS page composition
|
||||
sales-quote/ # Quote page composition
|
||||
widgets/
|
||||
inspection-report/ # Self-contained inspection UI
|
||||
ui/
|
||||
model/
|
||||
api/
|
||||
quote-calculator/
|
||||
features/
|
||||
add-inspection-item/
|
||||
approve-quote/
|
||||
entities/
|
||||
inspection/
|
||||
ui/ (InspectionCard, InspectionRow)
|
||||
model/ (types, store)
|
||||
api/ (getInspection, updateInspection)
|
||||
quote/
|
||||
ui/
|
||||
model/
|
||||
api/
|
||||
shared/
|
||||
ui/ (Button, Table, Modal -- your atoms)
|
||||
lib/ (formatDate, exportUtils)
|
||||
api/ (httpClient, apiProxy)
|
||||
config/ (constants)
|
||||
```
|
||||
|
||||
**Sources**:
|
||||
- [Feature-Sliced Design](https://feature-sliced.design/)
|
||||
- [Layers Reference](https://feature-sliced.design/docs/reference/layers)
|
||||
- [Slices and Segments](https://feature-sliced.design/docs/reference/slices-segments)
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Atomic Design -- Aging for App-Level Organization
|
||||
|
||||
**SAM's Current Approach**:
|
||||
```
|
||||
components/
|
||||
atoms/ # Basic UI elements
|
||||
molecules/ # (unused)
|
||||
organisms/ # Complex composed components
|
||||
templates/ # Page layout templates
|
||||
```
|
||||
|
||||
**Industry Assessment (2025-2026)**:
|
||||
- Atomic Design excels for **UI component libraries** (shared/ layer)
|
||||
- Struggles with **domain complexity** -- "UserCard" and "ProductCard" are both organisms but semantically distinct
|
||||
- Grouping by visual complexity (atom/molecule/organism) dilutes domain boundaries
|
||||
- Most large-scale projects have moved to **feature/domain organization** for application code
|
||||
- Atomic Design remains valuable for the **shared UI kit layer only**
|
||||
|
||||
**Sources**:
|
||||
- [Atomic Design Meets Feature-Based Architecture](https://medium.com/@buwanekasumanasekara/atomic-design-meets-feature-based-architecture-in-next-js-a-practical-guide-c06ea56cf5cc)
|
||||
- [From Components to Systems](https://www.codewithseb.com/blog/from-components-to-systems-scalable-frontend-with-atomiec-design)
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Modular Monolith (Frontend)
|
||||
|
||||
**Key Principles for ERP**:
|
||||
- Single deployment, but internally organized as independent modules
|
||||
- Each module = bounded context with clear API boundaries
|
||||
- Modules communicate through well-defined interfaces, not direct imports
|
||||
- Common concerns (auth, logging) handled at application level
|
||||
|
||||
**Applied to Next.js ERP**:
|
||||
```
|
||||
src/
|
||||
modules/
|
||||
quality/
|
||||
components/
|
||||
hooks/
|
||||
actions/
|
||||
types/
|
||||
index.ts # Public API -- only exports from here
|
||||
sales/
|
||||
components/
|
||||
hooks/
|
||||
actions/
|
||||
types/
|
||||
index.ts
|
||||
accounting/
|
||||
...
|
||||
shared/ # Cross-module utilities
|
||||
app/ # Next.js routing (thin layer)
|
||||
```
|
||||
|
||||
**Sources**:
|
||||
- [Modular Monolith Revolution](https://medium.com/@bhargavkoya56/the-modular-monolith-revolution-enterprise-grade-architecture-part-i-theory-b3705ca70a5f)
|
||||
- [Frontend at Scale](https://frontendatscale.com/issues/45/)
|
||||
|
||||
---
|
||||
|
||||
## 3. Server Actions Organization Patterns
|
||||
|
||||
### Pattern A: Colocated (SAM's Current -- 89 files)
|
||||
```
|
||||
app/[locale]/(protected)/quality/qms/
|
||||
page.tsx
|
||||
actions.ts # Server actions for this route
|
||||
```
|
||||
**Pros**: Easy to find, clear ownership
|
||||
**Cons**: Duplication across similar pages, no reuse
|
||||
|
||||
### Pattern B: Domain-Centralized (react-admin / Refine style)
|
||||
```
|
||||
src/
|
||||
actions/
|
||||
quality/
|
||||
inspection.ts # All inspection-related server actions
|
||||
qms.ts
|
||||
sales/
|
||||
quote.ts
|
||||
order.ts
|
||||
lib/
|
||||
api-client.ts # Shared fetch logic with auth
|
||||
```
|
||||
**Pros**: Reusable across pages, easier to maintain
|
||||
**Cons**: Indirection, harder to find for route-specific logic
|
||||
|
||||
### Pattern C: Hybrid (Recommended for large apps)
|
||||
```
|
||||
app/[locale]/(protected)/quality/qms/
|
||||
page.tsx
|
||||
_actions.ts # Route-specific actions only
|
||||
|
||||
src/
|
||||
domains/
|
||||
quality/
|
||||
actions/ # Shared domain actions
|
||||
inspection.ts
|
||||
qms.ts
|
||||
hooks/
|
||||
types/
|
||||
```
|
||||
**Pros**: Route-specific stays colocated, shared logic centralized
|
||||
**Cons**: Need clear rules on what goes where
|
||||
|
||||
### Industry Consensus
|
||||
For 100+ page apps, the **hybrid approach** (Pattern C) dominates. Route-specific logic stays colocated; shared domain logic is centralized. The key is having a clear **data provider / API client** layer that all server actions delegate to.
|
||||
|
||||
**Sources**:
|
||||
- [Next.js Colocation Template](https://next-colocation-template.vercel.app/)
|
||||
- [Inside the App Router (2025)](https://medium.com/better-dev-nextjs-react/inside-the-app-router-best-practices-for-next-js-file-and-directory-structure-2025-edition-ed6bc14a8da3)
|
||||
|
||||
---
|
||||
|
||||
## 4. CRUD Abstraction Patterns for 50+ Similar Pages
|
||||
|
||||
### Pattern 1: Resource Hooks (react-admin / Refine approach)
|
||||
```typescript
|
||||
// hooks/useResourceList.ts
|
||||
function useResourceList<T>(resource: string, options?: ListOptions) {
|
||||
const [data, setData] = useState<T[]>([]);
|
||||
const [pagination, setPagination] = useState({ page: 1, pageSize: 20 });
|
||||
const [filters, setFilters] = useState({});
|
||||
const [sorters, setSorters] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
fetchList(resource, { pagination, filters, sorters })
|
||||
.then(result => setData(result.data));
|
||||
}, [resource, pagination, filters, sorters]);
|
||||
|
||||
return { data, pagination, setPagination, filters, setFilters, sorters, setSorters };
|
||||
}
|
||||
|
||||
// Usage in any list page
|
||||
function QualityInspectionList() {
|
||||
const { data, pagination, filters } = useResourceList<Inspection>('quality/inspections');
|
||||
return <UniversalListPage data={data} columns={inspectionColumns} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Config-Driven Pages (Payload CMS approach)
|
||||
```typescript
|
||||
// configs/quality-inspection.config.ts
|
||||
export const inspectionConfig: ResourceConfig = {
|
||||
resource: 'quality/inspections',
|
||||
list: {
|
||||
columns: [
|
||||
{ key: 'id', label: '번호' },
|
||||
{ key: 'name', label: '검사명' },
|
||||
{ key: 'status', label: '상태', render: StatusBadge },
|
||||
],
|
||||
filters: [
|
||||
{ key: 'status', type: 'select', options: statusOptions },
|
||||
{ key: 'dateRange', type: 'daterange' },
|
||||
],
|
||||
defaultSort: { key: 'createdAt', direction: 'desc' },
|
||||
},
|
||||
form: {
|
||||
fields: [
|
||||
{ name: 'name', type: 'text', required: true, label: '검사명' },
|
||||
{ name: 'type', type: 'select', options: typeOptions, label: '검사유형' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Generic page component
|
||||
function ResourceListPage({ config }: { config: ResourceConfig }) {
|
||||
const list = useResourceList(config.resource);
|
||||
return <UniversalListPage {...list} columns={config.list.columns} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: Template Composition (SAM's current direction, improved)
|
||||
```typescript
|
||||
// templates/UniversalCRUDPage.tsx -- enhanced version
|
||||
function UniversalCRUDPage<T>({
|
||||
resource,
|
||||
listConfig,
|
||||
detailConfig,
|
||||
formConfig,
|
||||
}: CRUDPageProps<T>) {
|
||||
// Handles list/detail/form modes based on URL
|
||||
// Integrates data fetching, pagination, filtering
|
||||
// Renders appropriate template based on mode
|
||||
}
|
||||
```
|
||||
|
||||
### Industry Assessment
|
||||
- **Pattern 1** (Resource Hooks) is the most widely adopted -- used by react-admin (25K stars) and Refine (30K stars)
|
||||
- **Pattern 2** (Config-Driven) reduces code the most but requires upfront investment in the config system
|
||||
- **Pattern 3** (Template Composition) is the middle ground -- SAM's `UniversalListPage` is already this direction
|
||||
|
||||
**Recommendation**: Evolve toward a **Provider + Resource Hooks** layer. Keep `UniversalListPage` and `IntegratedDetailTemplate` but back them with a standardized data provider.
|
||||
|
||||
---
|
||||
|
||||
## 5. Comparison Matrix: SAM ERP vs Industry Patterns
|
||||
|
||||
| Dimension | SAM ERP (Current) | react-admin | Refine | Payload CMS | FSD | Recommendation |
|
||||
|-----------|-------------------|-------------|--------|-------------|-----|----------------|
|
||||
| **Folder Structure** | Domain-based (app router) | Resource-based | Resource-based | Collection-based | Layer > Slice > Segment | Hybrid Domain + FSD shared layer |
|
||||
| **Component Org** | Atomic Design (partial) | Flat per resource | Flat per resource | Config-driven | Layer-based (entities/features) | FSD for app code, Atomic for shared UI |
|
||||
| **API Layer** | 89 colocated actions.ts | Centralized dataProvider | Centralized dataProvider | Built-in Local API | api/ segment per slice | Centralized API client + domain actions |
|
||||
| **CRUD Abstraction** | UniversalListPage template | Resource + Controller hooks | useTable/useForm hooks | Auto-generated from config | Manual per feature | Add resource hooks on top of templates |
|
||||
| **Form Handling** | Mixed (migrating to RHF+Zod) | react-hook-form (built-in) | react-hook-form (headless) | Auto from field config | Manual per feature | Complete RHF+Zod migration |
|
||||
| **State Management** | Zustand stores | React Query (built-in) | React Query (built-in) | Server-side | Per-slice model/ | Keep Zustand for UI state, add React Query for server state |
|
||||
| **Type Safety** | Manual interfaces | Built-in types | TypeScript throughout | Auto-generated from schema | Manual per segment | Consider schema-driven type generation |
|
||||
| **50+ Page Scale** | Manual duplication | Resource registration | Inferencer + hooks | Collection config | Slice per entity | Resource hooks + config-driven columns |
|
||||
|
||||
---
|
||||
|
||||
## 6. Actionable Recommendations for SAM ERP
|
||||
|
||||
### Priority 1: Introduce a Data Provider / API Client Layer
|
||||
**Why**: The biggest gap vs. industry standard. 89 scattered actions.ts files means duplicated fetch logic, inconsistent error handling, and no centralized caching.
|
||||
|
||||
**Action**: Create a `dataProvider` abstraction inspired by react-admin/Refine:
|
||||
```typescript
|
||||
// src/lib/data-provider.ts
|
||||
export const dataProvider = {
|
||||
getList: (resource, params) => proxyFetch(`/api/proxy/${resource}`, params),
|
||||
getOne: (resource, id) => proxyFetch(`/api/proxy/${resource}/${id}`),
|
||||
create: (resource, data) => proxyFetch(`/api/proxy/${resource}`, { method: 'POST', body: data }),
|
||||
update: (resource, id, data) => proxyFetch(`/api/proxy/${resource}/${id}`, { method: 'PUT', body: data }),
|
||||
delete: (resource, id) => proxyFetch(`/api/proxy/${resource}/${id}`, { method: 'DELETE' }),
|
||||
};
|
||||
```
|
||||
|
||||
### Priority 2: Create Resource Hooks
|
||||
**Why**: Reduce per-page boilerplate for list/detail/form patterns.
|
||||
|
||||
**Action**: Build `useResourceList`, `useResourceDetail`, `useResourceForm` hooks that wrap the data provider.
|
||||
|
||||
### Priority 3: Evolve Folder Structure Toward Hybrid FSD
|
||||
**Why**: Atomic Design for app-level code leads to unclear domain boundaries.
|
||||
|
||||
**Action**:
|
||||
- Keep `shared/ui/` (atoms/organisms) for reusable UI components
|
||||
- Add `domains/` or `entities/` for business-logic grouping
|
||||
- Keep `app/` routes thin -- delegate to domain components
|
||||
|
||||
### Priority 4: Complete Form Standardization
|
||||
**Why**: Mixed form patterns make maintenance harder and prevent reusable form configs.
|
||||
|
||||
**Action**: Complete the react-hook-form + Zod migration. Consider field-config-driven forms (Payload pattern) for highly repetitive forms.
|
||||
|
||||
### Priority 5: Consider Server State Management (React Query / TanStack Query)
|
||||
**Why**: react-admin and Refine both use React Query internally for caching, optimistic updates, and background refetching. Zustand is better suited for client UI state.
|
||||
|
||||
**Action**: Evaluate adding TanStack Query for server state alongside Zustand for UI state.
|
||||
|
||||
---
|
||||
|
||||
## 7. What SAM ERP Is Already Doing Well
|
||||
|
||||
1. **Domain-based routing** (`app/[locale]/(protected)/quality/...`) aligns with industry best practice
|
||||
2. **UniversalListPage + IntegratedDetailTemplate** is the right abstraction direction (similar to react-admin's List/Edit components)
|
||||
3. **SearchableSelectionModal** as a reusable pattern is good (similar to react-admin's ReferenceInput)
|
||||
4. **Server Actions in colocated files** follows Next.js official recommendation for route-specific logic
|
||||
5. **Zustand for global state** is a solid choice for UI state (sidebar state, theme, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
### Open-Source Projects
|
||||
- [react-admin - Architecture](https://marmelab.com/react-admin/Architecture.html)
|
||||
- [react-admin - GitHub](https://github.com/marmelab/react-admin)
|
||||
- [Refine - Data Provider](https://refine.dev/docs/data/data-provider/)
|
||||
- [Refine - GitHub](https://github.com/refinedev/refine)
|
||||
- [Payload CMS - Collections](https://payloadcms.com/docs/configuration/collections)
|
||||
- [Payload CMS - GitHub](https://github.com/payloadcms/payload)
|
||||
- [Medusa - Admin Development](https://docs.medusajs.com/learn/fundamentals/admin)
|
||||
- [Medusa - GitHub](https://github.com/medusajs/medusa)
|
||||
|
||||
### Architectural Methodologies
|
||||
- [Feature-Sliced Design](https://feature-sliced.design/)
|
||||
- [FSD - Layers Reference](https://feature-sliced.design/docs/reference/layers)
|
||||
- [Atomic Design + FSD Hybrid](https://medium.com/@buwanekasumanasekara/atomic-design-meets-feature-based-architecture-in-next-js-a-practical-guide-c06ea56cf5cc)
|
||||
- [Clean Architecture vs FSD in Next.js](https://medium.com/@metastability/clean-architecture-vs-feature-sliced-design-in-next-js-applications-04df25e62690)
|
||||
|
||||
### Folder Structure & Patterns
|
||||
- [Next.js App Router Best Practices (2025)](https://medium.com/better-dev-nextjs-react/inside-the-app-router-best-practices-for-next-js-file-and-directory-structure-2025-edition-ed6bc14a8da3)
|
||||
- [Scalable Next.js Folder Structure](https://techtales.vercel.app/read/thedon/building-a-scalable-folder-structure-for-large-next-js-projects)
|
||||
- [SaaS Architecture Patterns with Next.js](https://vladimirsiedykh.com/blog/saas-architecture-patterns-nextjs)
|
||||
- [Modular Monolith for Frontend](https://frontendatscale.com/issues/45/)
|
||||
@@ -0,0 +1,224 @@
|
||||
# 동적 렌더링 플랫폼 전략 — 기준관리 기반 화면 자동 구성
|
||||
|
||||
> 작성일: 2026-02-19
|
||||
> 상태: 비전 정리 (논의 기반)
|
||||
> 관련 기술 설계: `[DESIGN-2026-02-11] dynamic-field-type-extension.md`
|
||||
> 관련 구현 현황: `[IMPL-2026-02-11] dynamic-field-components.md`
|
||||
> 관련 로드맵: `item-master/[DESIGN-2025-12-12] item-master-form-builder-roadmap.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 핵심 비전
|
||||
|
||||
```
|
||||
기준관리 페이지에서 설정 → API로 메타데이터 전달 → 프론트가 자동 렌더링
|
||||
```
|
||||
|
||||
**목표**: 개발자가 매번 화면을 코딩하는 것이 아니라, 기준관리 페이지에서 등록한 설정값에 따라 프론트엔드가 동적으로 화면을 구성하는 **ERP 커스터마이징 플랫폼**.
|
||||
|
||||
---
|
||||
|
||||
## 2. 운영 워크플로우 비전
|
||||
|
||||
### 2.1 전체 흐름
|
||||
|
||||
```
|
||||
현장 방문 (영업자/매니저)
|
||||
├─ 녹음, 체크리스트, 문서 수집
|
||||
└─ → MD 파일 정리 (요구사항)
|
||||
│
|
||||
↓
|
||||
기준관리 페이지 (관리자/컨설턴트)
|
||||
├─ MD 보고 속성, 칼럼, 옵션 등록
|
||||
└─ → 메타데이터 저장 (DB)
|
||||
│
|
||||
↓ API
|
||||
│
|
||||
프론트엔드 (자동 렌더링)
|
||||
└─ 메타데이터 기반으로 동적 화면 구성
|
||||
```
|
||||
|
||||
### 2.2 역할 변화
|
||||
|
||||
| 역할 | 현재 | 비전 |
|
||||
|------|------|------|
|
||||
| 영업자/매니저 | 요구사항 전달 → 개발 대기 | 현장에서 MD 파일 작성 |
|
||||
| 관리자/컨설턴트 | — | MD 보고 기준관리에 설정 입력 |
|
||||
| **개발자** | **요구사항마다 화면 코딩** | **플랫폼 유지보수 + 새 블록 타입 추가 시에만 개입** |
|
||||
|
||||
### 2.3 개발자 개입이 필요한 시점
|
||||
|
||||
- 기존 블록(Input, Select, DatePicker 등)으로 조합 가능 → **개발자 불필요**
|
||||
- 새로운 입력 타입/계산 로직 필요 → **블록 1개 추가** → 이후 재사용
|
||||
- 기준관리 UI 자체 개선 → **설계/검증**
|
||||
- page-builder 고도화 → **설계/구현**
|
||||
|
||||
---
|
||||
|
||||
## 3. 현재 자산 현황
|
||||
|
||||
### 3.1 이미 있는 것
|
||||
|
||||
#### UI 블록 (공통 컴포넌트)
|
||||
```
|
||||
src/components/ui/
|
||||
├─ Input, NumberInput, QuantityInput, CurrencyInput
|
||||
├─ Select, Checkbox, DatePicker, Textarea
|
||||
├─ Button, Badge, Card, Dialog
|
||||
└─ ...
|
||||
```
|
||||
모든 도메인별 테이블이 이 공통 블록을 사용 중.
|
||||
|
||||
#### 동적 필드 시스템 (14종 완성)
|
||||
```
|
||||
DynamicItemForm/fields/
|
||||
├─ 기존 6종: textbox, number, dropdown, checkbox, date, textarea
|
||||
└─ 신규 8종: reference, multi-select, file, currency, unit-value, radio, toggle, computed
|
||||
```
|
||||
Phase 1~3 프론트 구현 완료 (백엔드 작업 대기).
|
||||
|
||||
#### 범용 테이블 섹션
|
||||
```
|
||||
DynamicTableSection — config 기반 칼럼 정의, 행 CRUD, 요약행
|
||||
TableCellRenderer — 테이블 셀 = DynamicFieldRenderer 재사용
|
||||
```
|
||||
|
||||
#### 속성 관리 시스템 (품목기준관리)
|
||||
```
|
||||
useAttributeManagement — 속성 옵션 상태 관리
|
||||
AttributeTabContent — 동적 탭 렌더링
|
||||
OptionColumn[] + MasterOption[] — 메타데이터 구조
|
||||
```
|
||||
|
||||
#### page-builder 프로토타입
|
||||
```
|
||||
/dev/page-builder — 드래그앤드롭, 섹션/필드 구성, Undo/Redo, 반응형 뷰포트
|
||||
```
|
||||
|
||||
### 3.2 현재 구조: "기준관리 → 동적 렌더링" 패턴
|
||||
|
||||
```
|
||||
품목기준관리 (Admin) 품목 등록 (User)
|
||||
ItemMasterDataManagement.tsx DynamicItemForm/index.tsx
|
||||
↓ 설정 (pages/sections/fields) ↓ 읽어서 렌더링
|
||||
DB에 메타데이터 저장 DynamicFieldRenderer (14종 switch)
|
||||
DynamicTableSection (config 기반)
|
||||
```
|
||||
|
||||
**이 패턴이 핵심이고, 다른 도메인에도 동일하게 확장하는 것이 비전.**
|
||||
|
||||
---
|
||||
|
||||
## 4. 확장 대상 분석
|
||||
|
||||
### 4.1 도메인별 동적 렌더링 적합성
|
||||
|
||||
| 도메인 | 적합도 | 이유 |
|
||||
|--------|:---:|------|
|
||||
| 품목기준관리 | ✅ 이미 적용 | 테넌트/업종별 관리 항목이 다름 |
|
||||
| 설비/자산 관리 | ✅ 높음 | 설비 종류별 관리 속성이 다름 |
|
||||
| 거래처 관리 | ✅ 높음 | 업종별 추가 정보 다름 |
|
||||
| 공정/라우팅 관리 | ✅ 높음 | 제조 방식별 공정 구성 다름 |
|
||||
| 검사 항목 관리 | ✅ 높음 | 품목별 검사 항목/기준 다름 |
|
||||
| 견적서/발주서 | 🟡 부분 | 테이블은 동적 가능, 비즈니스 로직은 고정 |
|
||||
| 세금계산서 | ❌ 낮음 | 법정 양식, 테넌트별 차이 없음 |
|
||||
| 대시보드 | ❌ 낮음 | 위젯 기반이 더 적합 |
|
||||
|
||||
### 4.2 편집 가능 테이블 현황
|
||||
|
||||
| 컴포넌트 | 공통 컴포넌트 사용 | 자동 계산 | 합계 행 |
|
||||
|---------|:---:|:---:|:---:|
|
||||
| EditableTable (공통) | 본인이 공통 | ❌ | ❌ |
|
||||
| TaxInvoiceItemTable | ❌ 개별 | ✅ | ✅ |
|
||||
| OrderDetailItemTable | ❌ 개별 | ❌ | ✅ |
|
||||
| EstimateDetailTableSection | ❌ 개별 | ✅ (복잡) | ✅ |
|
||||
| DynamicTableSection | ❌ 개별 (config 기반) | ✅ (요약) | ✅ |
|
||||
|
||||
**테이블 안의 부품(Input, Select 등)은 전부 공통 ui 컴포넌트 사용.**
|
||||
껍데기(테이블 구조, 계산 로직)만 각자 구현.
|
||||
|
||||
---
|
||||
|
||||
## 5. page-builder 갭 분석
|
||||
|
||||
### 5.1 현재 page-builder 상태
|
||||
|
||||
```
|
||||
/dev/page-builder (프로토타입)
|
||||
✅ 드래그앤드롭 (섹션/필드 → 캔버스)
|
||||
✅ 섹션 타입 (BASIC, BOM, CUSTOM)
|
||||
✅ 필드 타입 (기본 6종)
|
||||
✅ 조건부 표시 (DisplayCondition)
|
||||
✅ 검증 규칙 (ValidationRule)
|
||||
✅ BOM 테이블
|
||||
✅ 마스터 필드 연동
|
||||
✅ Undo/Redo 히스토리
|
||||
✅ 반응형 뷰포트 (desktop/tablet/mobile)
|
||||
✅ API 변환 타입 정의
|
||||
```
|
||||
|
||||
### 5.2 비전 대비 부족한 점
|
||||
|
||||
| 항목 | 현재 | 필요 |
|
||||
|------|------|------|
|
||||
| 대상 도메인 | 품목 전용 (ItemType: FG/PT/SM/RM/CS) | 모든 기준관리 |
|
||||
| 사용자 | 개발자용 프로토타입 | 비개발자(영업/매니저/관리자) |
|
||||
| 테이블 섹션 | BOM만 (고정 칼럼) | 동적 칼럼 + 행 CRUD (DynamicTableSection 연결) |
|
||||
| 신규 필드 타입 | 기본 6종만 | 14종 전체 반영 |
|
||||
| API 연동 | 타입만 정의 | 실제 저장/조회 |
|
||||
| 프리셋 | 하드코딩 | 산업별 섹션 프리셋 선택 |
|
||||
|
||||
### 5.3 고도화 방향
|
||||
|
||||
```
|
||||
1단계: 도메인 범용화
|
||||
- ItemType 종속 제거
|
||||
- "기준관리 도메인" 선택 → 해당 도메인의 페이지 구성
|
||||
|
||||
2단계: 14종 필드 타입 반영
|
||||
- ComponentPalette에 신규 8종 필드 추가
|
||||
- PropertyPanel에 각 필드별 config 편집 UI
|
||||
|
||||
3단계: DynamicTableSection 연결
|
||||
- BOM 외 범용 테이블 섹션 지원
|
||||
- 칼럼 정의 UI (타입/너비/필수 설정)
|
||||
|
||||
4단계: 비개발자 UX
|
||||
- 용어 단순화 (field_type → "입력 형태")
|
||||
- 미리보기 강화
|
||||
- 저장/불러오기
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 4-Level 아키텍처 요약
|
||||
|
||||
기존 기술 설계서(`[DESIGN-2026-02-11]`)의 핵심:
|
||||
|
||||
```
|
||||
Level 1: 필드 타입 컴포넌트 (14종) — 코드 레벨, 거의 안 바뀜
|
||||
Level 2: properties config (JSON) — 설정 레벨, 코드 변경 없음
|
||||
Level 3: 섹션 프리셋 (JSON) — 템플릿 레벨, 코드 변경 없음
|
||||
Level 4: reference sources (API URL) — 연결 레벨, 코드 변경 없음
|
||||
```
|
||||
|
||||
**새 산업 진출 시에도 프론트엔드 코드 변경 = 0줄.**
|
||||
백엔드 API + config JSON만 추가.
|
||||
|
||||
---
|
||||
|
||||
## 7. 관련 문서
|
||||
|
||||
| 문서 | 위치 | 내용 |
|
||||
|------|------|------|
|
||||
| 동적 필드 타입 확장 설계 | `architecture/[DESIGN-2026-02-11]` | 4-Level 구조, 14종 필드, 범용 테이블, 산업별 확장 |
|
||||
| 동적 필드 컴포넌트 구현 | `architecture/[IMPL-2026-02-11]` | Phase 1~3 프론트 구현 완료 상태 |
|
||||
| Form Builder 로드맵 | `item-master/[DESIGN-2025-12-12]` | Low-Code Form Builder 초기 로드맵 |
|
||||
| 백엔드 API 스펙 | `item-master/[API-REQUEST-2026-02-12]` | 동적 필드 타입 백엔드 API 요청서 |
|
||||
| page-builder 참조 | `dev/[REF] page-builder-implementation.md` | 페이지 빌더 구현 참조 |
|
||||
| 멀티테넌시 최적화 | `architecture/[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md` | 테넌트별 격리/최적화 |
|
||||
|
||||
---
|
||||
|
||||
**문서 버전**: 1.0
|
||||
**마지막 업데이트**: 2026-02-19
|
||||
BIN
claudedocs/archive/qa-inbox-modal-test.png
Normal file
BIN
claudedocs/archive/qa-inbox-modal-test.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 734 KiB |
BIN
claudedocs/archive/qa-reference-modal-test.png
Normal file
BIN
claudedocs/archive/qa-reference-modal-test.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 712 KiB |
@@ -0,0 +1,97 @@
|
||||
# [NEXT-2025-12-22] 생산 현황판 세션 컨텍스트
|
||||
|
||||
## 세션 요약 (2025-12-22)
|
||||
|
||||
### 완료된 작업 ✅
|
||||
- [x] Phase 1: 생산 현황판 메인 페이지 구현
|
||||
- [x] Phase 2: 작업자 화면 구현 (별도 페이지)
|
||||
- [x] Phase 3: 전량완료 기능 (확인/완료 팝업, 뱃지)
|
||||
- [x] Phase 4: 공정상세 섹션 구현 (카드 내 토글)
|
||||
- [x] Phase 5: 자재투입 모달 구현
|
||||
- [x] Phase 6: 작업일지 모달 구현 (⚠️ 개선 필요)
|
||||
- [x] Phase 7: 이슈보고 모달 구현
|
||||
- [x] Phase 8: 네비게이션 연결 (TODO 주석 처리)
|
||||
|
||||
### 다음 세션 TODO ⚠️
|
||||
|
||||
#### 1. 작업일지 모달 개선 (우선)
|
||||
**현재**: 단순 테이블 형태로 구현됨
|
||||
**요청**: 기안함 상세 화면 스타일 (완성된 문서 형태)로 개선
|
||||
|
||||
**참고 컴포넌트**:
|
||||
```
|
||||
src/components/approval/DocumentDetail/
|
||||
├── ProposalDocument.tsx ← 기품의서 양식
|
||||
├── ExpenseReportDocument.tsx ← 지출보고서 양식
|
||||
└── ExpenseEstimateDocument.tsx ← 지출품의서 양식
|
||||
```
|
||||
|
||||
**수정 대상**:
|
||||
```
|
||||
src/components/production/WorkerScreen/WorkLogModal.tsx
|
||||
```
|
||||
|
||||
**작업 내용**:
|
||||
- DocumentDetail 컴포넌트 스타일 참고
|
||||
- 완성된 문서 형태로 작업일지 양식 재구현
|
||||
- 인쇄 친화적 레이아웃 적용
|
||||
|
||||
#### 2. 작업지시 관리 페이지 (대기)
|
||||
- 생산 현황판에서 네비게이션 연결 대기
|
||||
- 스크린샷/설명 별도 제공 예정
|
||||
|
||||
---
|
||||
|
||||
### 생성된 파일 목록
|
||||
|
||||
```
|
||||
src/app/[locale]/(protected)/production/
|
||||
├── dashboard/page.tsx ✅
|
||||
└── worker-screen/page.tsx ✅
|
||||
|
||||
src/components/production/
|
||||
├── ProductionDashboard/
|
||||
│ ├── index.tsx ✅
|
||||
│ ├── types.ts ✅
|
||||
│ └── mockData.ts ✅
|
||||
│
|
||||
└── WorkerScreen/
|
||||
├── index.tsx ✅
|
||||
├── types.ts ✅
|
||||
├── WorkCard.tsx ✅
|
||||
├── ProcessDetailSection.tsx ✅
|
||||
├── MaterialInputModal.tsx ✅
|
||||
├── WorkLogModal.tsx ⚠️ 개선 필요
|
||||
├── IssueReportModal.tsx ✅
|
||||
├── CompletionConfirmDialog.tsx ✅
|
||||
└── CompletionToast.tsx ✅
|
||||
|
||||
src/components/ui/
|
||||
└── collapsible.tsx ✅ (신규 추가, @radix-ui/react-collapsible 설치됨)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 테스트 URL
|
||||
- 생산 현황판: http://localhost:3000/ko/production/dashboard
|
||||
- 작업자 화면: http://localhost:3000/ko/production/worker-screen
|
||||
|
||||
---
|
||||
|
||||
### 참고 사항
|
||||
1. **작업자 화면 = 별도 페이지** (생산 현황판 하위 아님)
|
||||
- 사이드바 메뉴로 접근
|
||||
- "돌아가기" 버튼 불필요
|
||||
|
||||
2. **모든 alert() → AlertDialog 변환 완료**
|
||||
- 전량완료 확인/성공
|
||||
- 이슈보고 벨리데이션/성공
|
||||
|
||||
3. **공정상세 = 카드 내 토글 확장**
|
||||
- Collapsible 컴포넌트 사용
|
||||
- 5단계 공정 표시
|
||||
|
||||
---
|
||||
|
||||
**작성일**: 2025-12-22
|
||||
**상태**: 🔄 작업일지 모달 개선 대기
|
||||
@@ -0,0 +1,78 @@
|
||||
ㅏ# 세션 요약 (2025-12-30)
|
||||
|
||||
## 완료된 작업
|
||||
|
||||
### 1. fetch-wrapper 목적 확인
|
||||
- **목적**: 401 에러(세션 만료) 발생 시 로그인 리다이렉트를 **중앙화**
|
||||
- **장점**: 중복 코드 제거 + 새 작업자도 규칙 준수 가능
|
||||
|
||||
### 2. Accounting 도메인 완료 (12/12) ✅
|
||||
- [x] `SalesManagement/actions.ts`
|
||||
- [x] `VendorManagement/actions.ts`
|
||||
- [x] `PurchaseManagement/actions.ts`
|
||||
- [x] `DepositManagement/actions.ts`
|
||||
- [x] `WithdrawalManagement/actions.ts`
|
||||
- [x] `VendorLedger/actions.ts`
|
||||
- [x] `ReceivablesStatus/actions.ts`
|
||||
- [x] `ExpectedExpenseManagement/actions.ts`
|
||||
- [x] `CardTransactionInquiry/actions.ts`
|
||||
- [x] `DailyReport/actions.ts`
|
||||
- [x] `BadDebtCollection/actions.ts`
|
||||
- [x] `BankTransactionInquiry/actions.ts`
|
||||
|
||||
### 3. HR 도메인 진행중 (1/6)
|
||||
- [x] `EmployeeManagement/actions.ts` (이미 마이그레이션되어 있었음)
|
||||
- [~] `VacationManagement/actions.ts` (import만 변경됨, 함수 마이그레이션 필요)
|
||||
|
||||
## 다음 세션 TODO
|
||||
|
||||
### HR 도메인 나머지 (5개)
|
||||
- [ ] `VacationManagement/actions.ts` - 함수 마이그레이션 완료 필요
|
||||
- [ ] `SalaryManagement/actions.ts`
|
||||
- [ ] `CardManagement/actions.ts`
|
||||
- [ ] `DepartmentManagement/actions.ts`
|
||||
- [ ] `AttendanceManagement/actions.ts`
|
||||
|
||||
### 기타 도메인 (Approval, Production, Settings, 기타)
|
||||
- Approval: 4개
|
||||
- Production: 4개
|
||||
- Settings: 11개
|
||||
- 기타: 12개
|
||||
- 상세 목록은 체크리스트 문서 참고
|
||||
|
||||
### 빌드 검증
|
||||
- [ ] `npm run build` 실행하여 마이그레이션 검증
|
||||
|
||||
## 참고 사항
|
||||
|
||||
### 마이그레이션 패턴 (참고용)
|
||||
```typescript
|
||||
// Before
|
||||
import { cookies } from 'next/headers';
|
||||
async function getApiHeaders() { ... }
|
||||
const response = await fetch(url, { headers });
|
||||
|
||||
// After
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
if (error) return { success: false, error: error.message };
|
||||
```
|
||||
|
||||
### 주요 변경 포인트
|
||||
1. `getApiHeaders()` 함수 제거
|
||||
2. `import { cookies } from 'next/headers'` 제거
|
||||
3. `fetch()` → `serverFetch()` 변경
|
||||
4. `{ response, error }` 구조분해 사용
|
||||
5. 파일 다운로드(Excel/PDF)는 `cookies` import 유지 (custom Accept 헤더 필요)
|
||||
|
||||
### 특이사항
|
||||
- `EmployeeManagement/actions.ts`는 이미 `serverFetch` 사용 중이었음
|
||||
- `uploadProfileImage` 함수는 FormData 업로드라 `cookies` import 유지
|
||||
|
||||
## 체크리스트 문서
|
||||
`claudedocs/api/[IMPL-2025-12-30] fetch-wrapper-migration.md`
|
||||
|
||||
## 진행률
|
||||
- 전체: 49개 파일
|
||||
- 완료: 13개 (27%)
|
||||
- 남음: 36개
|
||||
@@ -0,0 +1,101 @@
|
||||
# 주일 거래처 관리 세션 컨텍스트
|
||||
|
||||
Last Updated: 2025-12-30
|
||||
|
||||
## 세션 요약 (2025-12-30)
|
||||
|
||||
### 완료된 작업
|
||||
- [x] 거래처 리스트 필터 위치 수정 (테이블 위로 이동)
|
||||
- [x] 거래처 폼 컴포넌트 생성 (PartnerForm.tsx)
|
||||
- [x] 등록 페이지 생성 (/new/page.tsx)
|
||||
- [x] 상세 페이지 생성 (/[id]/page.tsx)
|
||||
- [x] 수정 페이지 생성 (/[id]/edit/page.tsx)
|
||||
- [x] types.ts 확장 (전체 필드 추가)
|
||||
- [x] actions.ts CRUD 함수 추가
|
||||
|
||||
### 다음 세션 TODO
|
||||
- [ ] **회사 정보 + 신용/거래 정보 섹션 합치기** (스크린샷 기준으로 하나의 섹션)
|
||||
- [ ] 실제 API 연동
|
||||
|
||||
### 참고 사항
|
||||
- 스크린샷에서 "회사 정보"와 "신용/거래 정보"가 하나의 Card 섹션으로 되어 있음
|
||||
- 현재 코드는 별도 섹션으로 분리됨 → 합쳐야 함
|
||||
|
||||
---
|
||||
|
||||
## 완료된 작업 (전체)
|
||||
|
||||
### 1. 프로젝트 구조 설정
|
||||
- [x] `claudedocs/juil/` 문서 폴더 생성
|
||||
- [x] `[REF] juil-project-structure.md` 프로젝트 구조 가이드 작성
|
||||
- [x] `_index.md` 문서 맵에 juil 섹션 추가
|
||||
|
||||
### 2. 거래처 관리 리스트 페이지
|
||||
- [x] 페이지: `src/app/[locale]/(protected)/juil/project/bidding/partners/page.tsx`
|
||||
- [x] 컴포넌트: `src/components/business/juil/partners/PartnerListClient.tsx`
|
||||
- [x] 타입: `src/components/business/juil/partners/types.ts`
|
||||
- [x] 액션: `src/components/business/juil/partners/actions.ts` (목업 데이터)
|
||||
- [x] 인덱스: `src/components/business/juil/partners/index.ts`
|
||||
- [x] 레이아웃 수정: 필터를 테이블 위로 이동, 등록 버튼 상단 배치
|
||||
|
||||
### 3. 거래처 등록/상세/수정 페이지
|
||||
- [x] 폼 컴포넌트: `src/components/business/juil/partners/PartnerForm.tsx`
|
||||
- [x] 등록 페이지: `src/app/[locale]/(protected)/juil/project/bidding/partners/new/page.tsx`
|
||||
- [x] 상세 페이지: `src/app/[locale]/(protected)/juil/project/bidding/partners/[id]/page.tsx`
|
||||
- [x] 수정 페이지: `src/app/[locale]/(protected)/juil/project/bidding/partners/[id]/edit/page.tsx`
|
||||
|
||||
### 4. 구현된 기능
|
||||
|
||||
#### 리스트 페이지
|
||||
- 통계 카드 (전체 거래처 / 미등록)
|
||||
- 검색 (거래처명, 번호, 대표자, 담당자)
|
||||
- 탭 필터 (전체 / 신규)
|
||||
- 테이블 위 필터: `총 N건 | 전체 ▾ | 최신순 ▾`
|
||||
- 테이블 컬럼: 체크박스, 번호, 거래처번호, 구분, 거래처명, 대표자, 담당자, 전화번호, 매출 결제일, 악성채권, 작업
|
||||
- 행 선택 시 수정/삭제 버튼 표시
|
||||
- 일괄 삭제 다이얼로그
|
||||
- 페이지네이션
|
||||
- 모바일 카드 뷰
|
||||
|
||||
#### 폼 페이지 (등록/상세/수정 공통)
|
||||
- **기본 정보**: 사업자등록번호, 거래처코드, 거래처명, 대표자명, 거래처유형, 업태, 업종
|
||||
- **연락처 정보**: 주소 (우편번호 찾기 DAUM), 전화번호, 모바일, 팩스, 이메일
|
||||
- **담당자 정보**: 담당자명, 담당자 전화, 시스템 관리자
|
||||
- **회사 정보**: 회사 로고 (BLOB 업로드), 매출 결제일, 신용등급, 거래등급, 세금계산서 이메일
|
||||
- **추가 정보**: 미수금, 연체 (토글), 악성채권 (토글)
|
||||
- **메모**: 추가/삭제 기능
|
||||
- **필요 서류**: 파일 업로드 (드래그 앤 드롭)
|
||||
|
||||
#### 모드별 버튼 분기
|
||||
- **등록**: 취소 | 저장
|
||||
- **수정**: 삭제 | 수정
|
||||
- **상세**: 목록가기 | 수정
|
||||
|
||||
## 테스트 URL
|
||||
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| 거래처 관리 (리스트) | `/ko/juil/project/bidding/partners` | ✅ 완료 |
|
||||
| 거래처 등록 | `/ko/juil/project/bidding/partners/new` | ✅ 완료 |
|
||||
| 거래처 상세 | `/ko/juil/project/bidding/partners/1` | ✅ 완료 |
|
||||
| 거래처 수정 | `/ko/juil/project/bidding/partners/1/edit` | ✅ 완료 |
|
||||
|
||||
## 디렉토리 구조
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/[locale]/(protected)/juil/
|
||||
│ └── project/bidding/partners/
|
||||
│ ├── page.tsx ✅
|
||||
│ ├── new/page.tsx ✅
|
||||
│ └── [id]/
|
||||
│ ├── page.tsx ✅
|
||||
│ └── edit/page.tsx ✅
|
||||
│
|
||||
└── components/business/juil/partners/
|
||||
├── index.ts ✅
|
||||
├── types.ts ✅
|
||||
├── actions.ts ✅ (목업)
|
||||
├── PartnerListClient.tsx ✅
|
||||
└── PartnerForm.tsx ✅ (섹션 수정 필요)
|
||||
```
|
||||
512
claudedocs/auth/[IMPL-2025-12-30] token-refresh-caching.md
Normal file
512
claudedocs/auth/[IMPL-2025-12-30] token-refresh-caching.md
Normal file
@@ -0,0 +1,512 @@
|
||||
# Token Refresh Caching 구현 문서
|
||||
|
||||
> 작성일: 2025-12-30
|
||||
> 상태: 완료
|
||||
|
||||
## 1. 문제 상황
|
||||
|
||||
### 1.1 증상
|
||||
페이지 로드 시 여러 API 호출이 동시에 발생할 때, 일부 요청이 401 에러와 함께 실패하고 로그인 페이지로 리다이렉트되는 현상.
|
||||
|
||||
### 1.2 원인 분석
|
||||
`useEffect`에서 여러 API를 동시에 호출할 때 **refresh_token 충돌** 발생:
|
||||
|
||||
```
|
||||
시간 →
|
||||
────────────────────────────────────────────────────────────────────
|
||||
[요청 A] access_token 만료 → 401 → refresh_token 사용 → ✅ 새 토큰 발급 (기존 refresh_token 폐기)
|
||||
[요청 B] access_token 만료 → 401 → refresh_token 사용 → ❌ 실패 (이미 폐기된 토큰)
|
||||
[요청 C] access_token 만료 → 401 → refresh_token 사용 → ❌ 실패 (이미 폐기된 토큰)
|
||||
────────────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
**핵심 문제**: refresh_token은 일회용(One-Time Use)이므로, 첫 번째 요청이 사용하면 즉시 폐기됨.
|
||||
|
||||
### 1.3 영향 범위
|
||||
- **Proxy 경로** (`/api/proxy/*`): 클라이언트 → Next.js → PHP 백엔드
|
||||
- **Server Actions** (`serverFetch`): Server Component에서 직접 API 호출
|
||||
|
||||
---
|
||||
|
||||
## 2. 해결 방법: Request Coalescing (요청 병합) 패턴
|
||||
|
||||
### 2.1 패턴 설명
|
||||
동시에 발생하는 동일한 요청을 하나로 병합하여 처리하는 표준 패턴.
|
||||
|
||||
```
|
||||
시간 →
|
||||
────────────────────────────────────────────────────────────────────
|
||||
[요청 A] 401 → refresh 시작 (Promise 생성) → ✅ 새 토큰 → 캐시 저장
|
||||
[요청 B] 401 → 캐시된 Promise 대기 ────────→ ✅ 같은 새 토큰 사용
|
||||
[요청 C] 401 → 캐시된 Promise 대기 ────────→ ✅ 같은 새 토큰 사용
|
||||
────────────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
### 2.2 구현 특징
|
||||
- **5초 캐싱**: refresh 결과를 5초간 캐시
|
||||
- **Promise 공유**: 진행 중인 refresh Promise를 여러 요청이 공유
|
||||
- **모듈 레벨 캐시**: Proxy와 serverFetch가 동일한 캐시 공유
|
||||
|
||||
---
|
||||
|
||||
## 3. 구현 코드
|
||||
|
||||
### 3.1 파일 구조
|
||||
```
|
||||
src/lib/api/
|
||||
├── refresh-token.ts # 🆕 공통 토큰 갱신 모듈 (캐싱 로직 포함)
|
||||
├── fetch-wrapper.ts # serverFetch (import from refresh-token)
|
||||
└── errors.ts # 에러 타입 정의
|
||||
|
||||
src/app/api/proxy/
|
||||
└── [...path]/route.ts # Proxy (import from refresh-token)
|
||||
|
||||
src/app/api/auth/
|
||||
├── check/route.ts # 🔧 인증 확인 API (2026-01-08 통합)
|
||||
└── refresh/route.ts # 🔧 토큰 갱신 API (2026-01-08 통합)
|
||||
```
|
||||
|
||||
### 3.2 공통 모듈: `refresh-token.ts`
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 🔄 Refresh Token 공통 모듈
|
||||
*
|
||||
* 문제: useEffect에서 여러 API 동시 호출 시 refresh_token 충돌
|
||||
* 해결: 5초간 refresh 결과 캐싱 + Promise 공유
|
||||
*/
|
||||
|
||||
export type RefreshResult = {
|
||||
success: boolean;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
expiresIn?: number;
|
||||
};
|
||||
|
||||
// 캐시 상태 (모듈 레벨에서 공유)
|
||||
let refreshCache: {
|
||||
promise: Promise<RefreshResult> | null;
|
||||
timestamp: number;
|
||||
result: RefreshResult | null;
|
||||
} = {
|
||||
promise: null,
|
||||
timestamp: 0,
|
||||
result: null,
|
||||
};
|
||||
|
||||
const REFRESH_CACHE_TTL = 5000; // 5초
|
||||
|
||||
/**
|
||||
* 실제 토큰 갱신 수행 (내부 함수)
|
||||
*/
|
||||
async function doRefreshToken(refreshToken: string): Promise<RefreshResult> {
|
||||
try {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/refresh`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
},
|
||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
success: true,
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
expiresIn: data.expires_in,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('🔴 [RefreshToken] Token refresh error:', error);
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰 갱신 함수 (5초 캐싱 적용)
|
||||
*
|
||||
* 동시 요청 시:
|
||||
* 1. 캐시된 결과가 있으면 즉시 반환
|
||||
* 2. 진행 중인 refresh가 있으면 그 Promise를 기다림
|
||||
* 3. 둘 다 없으면 새 refresh 시작
|
||||
*/
|
||||
export async function refreshAccessToken(
|
||||
refreshToken: string,
|
||||
caller: string = 'unknown'
|
||||
): Promise<RefreshResult> {
|
||||
const now = Date.now();
|
||||
|
||||
// 1. 캐시된 결과가 유효하면 즉시 반환
|
||||
if (refreshCache.result?.success && now - refreshCache.timestamp < REFRESH_CACHE_TTL) {
|
||||
console.log(`🔵 [${caller}] Using cached refresh result`);
|
||||
return refreshCache.result;
|
||||
}
|
||||
|
||||
// 2. 진행 중인 refresh가 있으면 그 결과를 기다림
|
||||
if (refreshCache.promise && now - refreshCache.timestamp < REFRESH_CACHE_TTL) {
|
||||
console.log(`🔵 [${caller}] Waiting for ongoing refresh...`);
|
||||
return refreshCache.promise;
|
||||
}
|
||||
|
||||
// 3. 새 refresh 시작
|
||||
console.log(`🔄 [${caller}] Starting new refresh request...`);
|
||||
refreshCache.timestamp = now;
|
||||
refreshCache.result = null;
|
||||
|
||||
refreshCache.promise = doRefreshToken(refreshToken).then(result => {
|
||||
refreshCache.result = result;
|
||||
return result;
|
||||
});
|
||||
|
||||
return refreshCache.promise;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 사용 예시
|
||||
|
||||
**Proxy에서 사용:**
|
||||
```typescript
|
||||
// src/app/api/proxy/[...path]/route.ts
|
||||
import { refreshAccessToken } from '@/lib/api/refresh-token';
|
||||
|
||||
// 401 응답 시
|
||||
const refreshResult = await refreshAccessToken(refreshToken, 'PROXY');
|
||||
```
|
||||
|
||||
**serverFetch에서 사용:**
|
||||
```typescript
|
||||
// src/lib/api/fetch-wrapper.ts
|
||||
import { refreshAccessToken } from './refresh-token';
|
||||
|
||||
// 401 응답 시
|
||||
const refreshResult = await refreshAccessToken(refreshToken, 'serverFetch');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 시행착오 기록
|
||||
|
||||
### 4.1 초기 문제: 중복 구현
|
||||
처음에는 Proxy와 serverFetch에서 각각 캐싱 로직을 별도로 구현했음.
|
||||
|
||||
**문제점:**
|
||||
- 코드 중복 (~80줄씩)
|
||||
- 두 캐시가 분리되어 있어 비효율적
|
||||
- 유지보수 어려움
|
||||
|
||||
**해결:** 공통 모듈 `refresh-token.ts`로 통합
|
||||
|
||||
### 4.2 빌드 오류: .next 폴더 손상
|
||||
```
|
||||
Error: Cannot find module './4586.js'
|
||||
```
|
||||
|
||||
**원인:** 이전 빌드 아티팩트와 새 코드 간 충돌
|
||||
|
||||
**해결:**
|
||||
```bash
|
||||
rm -rf .next
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 4.3 런타임 오류: app-paths-manifest.json 누락
|
||||
```
|
||||
500 Error: .next/server/app-paths-manifest.json not found
|
||||
```
|
||||
|
||||
**원인:** 빌드 중 .next 폴더 손상
|
||||
|
||||
**해결:**
|
||||
```bash
|
||||
rm -rf .next
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 4.4 Safari 호환성 문제 (이전 세션에서 해결)
|
||||
Safari에서 `SameSite=Strict` + `Secure` 조합이 localhost에서 쿠키 저장 실패.
|
||||
|
||||
**해결:**
|
||||
- `SameSite=Strict` → `SameSite=Lax`
|
||||
- `Secure`는 프로덕션에서만 적용
|
||||
|
||||
---
|
||||
|
||||
## 5. 동작 흐름도
|
||||
|
||||
### 5.1 정상 흐름 (토큰 유효)
|
||||
```
|
||||
클라이언트 → Proxy/serverFetch → API 요청 → 200 OK → 응답 반환
|
||||
```
|
||||
|
||||
### 5.2 토큰 갱신 흐름 (단일 요청)
|
||||
```
|
||||
클라이언트 → Proxy/serverFetch → API 요청 → 401
|
||||
↓
|
||||
refreshAccessToken()
|
||||
↓
|
||||
새 토큰 발급 + 쿠키 저장
|
||||
↓
|
||||
원래 요청 재시도 → 200 OK
|
||||
```
|
||||
|
||||
### 5.3 토큰 갱신 흐름 (동시 요청 - 캐싱 적용)
|
||||
```
|
||||
[요청 A] → 401 → refreshAccessToken() → 새 refresh 시작 ──┐
|
||||
[요청 B] → 401 → refreshAccessToken() → Promise 대기 ────┼→ 같은 새 토큰 공유
|
||||
[요청 C] → 401 → refreshAccessToken() → Promise 대기 ────┘
|
||||
↓
|
||||
각자 원래 요청 재시도
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 설정 값
|
||||
|
||||
| 항목 | 값 | 설명 |
|
||||
|------|-----|------|
|
||||
| REFRESH_CACHE_TTL | 5초 | refresh 결과 캐시 유지 시간 |
|
||||
| access_token Max-Age | 7200초 (2시간) | API에서 전달받은 값 사용 |
|
||||
| refresh_token Max-Age | 604800초 (7일) | 장기 보관 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 로그 메시지
|
||||
|
||||
### 7.1 캐시 히트 (이미 갱신된 토큰 재사용)
|
||||
```
|
||||
🔵 [PROXY] Using cached refresh result (age: 1234ms)
|
||||
🔵 [serverFetch] Using cached refresh result (age: 1234ms)
|
||||
```
|
||||
|
||||
### 7.2 대기 중 (다른 요청이 갱신 중)
|
||||
```
|
||||
🔵 [PROXY] Waiting for ongoing refresh...
|
||||
🔵 [serverFetch] Waiting for ongoing refresh...
|
||||
```
|
||||
|
||||
### 7.3 새 갱신 시작
|
||||
```
|
||||
🔄 [PROXY] Starting new refresh request...
|
||||
🔄 [serverFetch] Starting new refresh request...
|
||||
✅ [RefreshToken] Token refreshed successfully
|
||||
```
|
||||
|
||||
### 7.4 갱신 실패
|
||||
```
|
||||
🔴 [RefreshToken] Token refresh failed: { status: 401, ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 관련 파일
|
||||
|
||||
| 파일 | 역할 | 통합일 |
|
||||
|------|------|--------|
|
||||
| `src/lib/api/refresh-token.ts` | 공통 토큰 갱신 모듈 (캐싱 로직) | 2025-12-30 |
|
||||
| `src/lib/api/fetch-wrapper.ts` | Server Actions용 fetch wrapper | 2025-12-30 |
|
||||
| `src/lib/utils/redirect-error.ts` | Next.js redirect 에러 감지 유틸리티 | 2026-01-08 |
|
||||
| `src/app/api/proxy/[...path]/route.ts` | 클라이언트 API 프록시 | 2025-12-30 |
|
||||
| `src/app/api/auth/login/route.ts` | 로그인 및 초기 토큰 설정 | - |
|
||||
| `src/app/api/auth/check/route.ts` | 인증 상태 확인 API | 2026-01-08 |
|
||||
| `src/app/api/auth/refresh/route.ts` | 토큰 갱신 프록시 API | 2026-01-08 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 이 패턴이 "편법"이 아닌 이유
|
||||
|
||||
### 9.1 업계 표준 패턴
|
||||
- **Request Coalescing / Request Deduplication**: 공식 명칭
|
||||
- React Query, SWR, Apollo Client 등에서 동일 패턴 사용
|
||||
- CDN (Cloudflare, Fastly)에서도 동일 원리 적용
|
||||
|
||||
### 9.2 설계 원칙 준수
|
||||
- **DRY**: 중복 요청 제거
|
||||
- **효율성**: 서버 부하 감소
|
||||
- **일관성**: 모든 요청이 같은 새 토큰 사용
|
||||
|
||||
### 9.3 향후 위험성 없음
|
||||
- 5초 TTL은 충분히 짧아 토큰 갱신 지연 문제 없음
|
||||
- 실패 시 다음 요청에서 새로 갱신 시도
|
||||
- 캐시는 메모리 기반이라 서버 재시작 시 자동 초기화
|
||||
|
||||
---
|
||||
|
||||
## 10. 업데이트 이력
|
||||
|
||||
### 10.0 [2026-01-15] 미들웨어 사전 갱신 기능 추가
|
||||
|
||||
**관련 문서:** `[IMPL-2026-01-15] middleware-pre-refresh.md`
|
||||
|
||||
Request Coalescing 패턴만으로는 auth/check + serverFetch 동시 호출 시 Race Condition이 완전히 해결되지 않아, **미들웨어에서 페이지 렌더링 전 토큰을 미리 갱신**하는 기능 추가.
|
||||
|
||||
두 기능은 상호 보완적:
|
||||
- **미들웨어 사전 갱신**: 페이지 로드 전 토큰 준비 (1차 방어)
|
||||
- **Request Coalescing**: API 호출 시 401 발생 시 중복 갱신 방지 (2차 방어)
|
||||
|
||||
### 10.1 [2026-01-08] 누락된 API 라우트 통합
|
||||
|
||||
**문제 발견:**
|
||||
`/api/auth/check`와 `/api/auth/refresh` 라우트가 공유 캐시를 사용하지 않고 자체 fetch 로직을 사용하고 있었음.
|
||||
|
||||
**증상:**
|
||||
```
|
||||
🔍 Refresh API response status: 401
|
||||
❌ Refresh API failed: 401 {"error":"리프레시 토큰이 유효하지 않거나 만료되었습니다","error_code":"TOKEN_EXPIRED"}
|
||||
⚠️ Returning 401 due to refresh failure
|
||||
GET /api/auth/check 401
|
||||
```
|
||||
|
||||
**원인:**
|
||||
1. `serverFetch`에서 refresh 성공 → Token Rotation으로 이전 refresh_token 폐기
|
||||
2. `/api/auth/check`가 동시에 호출됨
|
||||
3. 자체 fetch 로직으로 이미 폐기된 토큰 사용 시도 → 실패 → 로그인 페이지 이동
|
||||
|
||||
**해결:**
|
||||
두 파일 모두 `refreshAccessToken()` 공유 함수를 사용하도록 수정:
|
||||
|
||||
```typescript
|
||||
// src/app/api/auth/check/route.ts
|
||||
import { refreshAccessToken } from '@/lib/api/refresh-token';
|
||||
|
||||
const refreshResult = await refreshAccessToken(refreshToken, 'auth/check');
|
||||
```
|
||||
|
||||
```typescript
|
||||
// src/app/api/auth/refresh/route.ts
|
||||
import { refreshAccessToken } from '@/lib/api/refresh-token';
|
||||
|
||||
const refreshResult = await refreshAccessToken(refreshToken, 'api/auth/refresh');
|
||||
```
|
||||
|
||||
**결과:**
|
||||
모든 refresh 경로가 동일한 5초 캐시를 공유하여 Token Rotation 충돌 방지.
|
||||
|
||||
### 10.2 [2026-01-08] 53개 Server Actions 파일 수정
|
||||
|
||||
**문제:**
|
||||
`redirect('/login')` 호출 시 발생하는 `NEXT_REDIRECT` 에러가 catch 블록에서 잡혀 `{ success: false }` 반환 → 무한 루프
|
||||
|
||||
**해결:**
|
||||
모든 actions.ts 파일에 `isRedirectError` 처리 추가:
|
||||
|
||||
```typescript
|
||||
import { isRedirectError } from 'next/dist/client/components/redirect';
|
||||
|
||||
} catch (error) {
|
||||
if (isRedirectError(error)) throw error;
|
||||
// ... 기존 에러 처리
|
||||
}
|
||||
```
|
||||
|
||||
### 10.3 [2026-01-08] refresh 실패 결과 캐시 버그 수정
|
||||
|
||||
**문제:**
|
||||
refresh 실패 결과도 5초간 캐시되어, 후속 요청들이 모두 실패 결과를 받음.
|
||||
|
||||
**해결:**
|
||||
`refresh-token.ts`에서 성공한 결과만 캐시하도록 수정:
|
||||
|
||||
```typescript
|
||||
// 1. 캐시된 성공 결과가 유효하면 즉시 반환
|
||||
if (refreshCache.result && refreshCache.result.success && now - refreshCache.timestamp < REFRESH_CACHE_TTL) {
|
||||
return refreshCache.result;
|
||||
}
|
||||
|
||||
// 2-1. 이전 refresh가 실패했으면 캐시 초기화
|
||||
if (refreshCache.result && !refreshCache.result.success) {
|
||||
refreshCache.promise = null;
|
||||
refreshCache.result = null;
|
||||
}
|
||||
```
|
||||
|
||||
### 10.4 [2026-01-08] isRedirectError 자체 유틸리티 함수로 변경
|
||||
|
||||
**문제:**
|
||||
Next.js 내부 경로(`next/dist/client/components/redirect`)가 버전 15에서 `redirect-error`로 변경됨.
|
||||
내부 경로 의존 시 Next.js 업데이트마다 수정 필요.
|
||||
|
||||
**해결:**
|
||||
자체 유틸리티 함수 생성하여 Next.js 내부 경로 의존성 제거:
|
||||
|
||||
```typescript
|
||||
// src/lib/utils/redirect-error.ts
|
||||
export function isNextRedirectError(error: unknown): boolean {
|
||||
return (
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'digest' in error &&
|
||||
typeof (error as { digest: string }).digest === 'string' &&
|
||||
(error as { digest: string }).digest.startsWith('NEXT_REDIRECT')
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**장점:**
|
||||
- Next.js 버전 업데이트에 영향 안 받음
|
||||
- 내부 경로 의존성 제거
|
||||
- 한 곳에서 관리 가능
|
||||
|
||||
---
|
||||
|
||||
## 11. 신규 Server Actions 개발 가이드
|
||||
|
||||
### 11.1 필수 패턴
|
||||
|
||||
새로운 `actions.ts` 파일 생성 시 반드시 아래 패턴을 따라야 합니다:
|
||||
|
||||
```typescript
|
||||
'use server';
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
|
||||
export async function someAction(params: SomeParams): Promise<SomeResult> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/some-endpoint`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET', // 또는 POST, PUT, DELETE
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '요청 실패' };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { success: true, data };
|
||||
|
||||
} catch (error) {
|
||||
// ⚠️ 필수: redirect 에러는 다시 throw해야 함
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
|
||||
console.error('[SomeAction] error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 11.2 왜 isNextRedirectError 처리가 필수인가?
|
||||
|
||||
```
|
||||
serverFetch에서 401 응답 시:
|
||||
1. refresh_token으로 토큰 갱신 시도
|
||||
2. 갱신 실패 시 redirect('/login') 호출
|
||||
3. redirect()는 NEXT_REDIRECT 에러를 throw
|
||||
4. 이 에러가 catch에서 잡히면 → { success: false } 반환 → 무한 루프
|
||||
5. 이 에러를 다시 throw하면 → Next.js가 정상 리다이렉트 처리
|
||||
```
|
||||
|
||||
### 11.3 체크리스트
|
||||
|
||||
새 actions.ts 파일 생성 시:
|
||||
|
||||
- [ ] `import { isNextRedirectError } from '@/lib/utils/redirect-error';` 추가
|
||||
- [ ] `import { serverFetch } from '@/lib/api/fetch-wrapper';` 사용
|
||||
- [ ] 모든 catch 블록에 `if (isNextRedirectError(error)) throw error;` 추가
|
||||
- [ ] 파일 내 모든 export 함수에 동일 패턴 적용
|
||||
424
claudedocs/auth/[IMPL-2026-01-15] middleware-pre-refresh.md
Normal file
424
claudedocs/auth/[IMPL-2026-01-15] middleware-pre-refresh.md
Normal file
@@ -0,0 +1,424 @@
|
||||
# 미들웨어 토큰 사전 갱신 (Pre-Refresh) 구현 문서
|
||||
|
||||
> 작성일: 2026-01-15
|
||||
> 상태: 완료
|
||||
|
||||
## 1. 문제 상황
|
||||
|
||||
### 1.1 기존 Request Coalescing 패턴의 한계
|
||||
|
||||
`refresh-token.ts`의 5초 캐싱 패턴으로 동시 API 호출 시 중복 갱신은 방지했지만, **auth/check + serverFetch 동시 호출** 문제가 완전히 해결되지 않았음.
|
||||
|
||||
### 1.2 Race Condition 시나리오
|
||||
|
||||
```
|
||||
페이지 로드 시 (access_token 만료, refresh_token만 있는 상태)
|
||||
|
||||
시간 →
|
||||
────────────────────────────────────────────────────────────────────
|
||||
[페이지 렌더링 시작]
|
||||
↓
|
||||
[useEffect] → auth/check 호출 ─────┐
|
||||
[Server Component] → serverFetch ──┼─→ 둘 다 refresh_token 필요
|
||||
↓
|
||||
첫 번째가 갱신하면 두 번째는?
|
||||
(캐시 공유해도 타이밍 문제 발생 가능)
|
||||
────────────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
### 1.3 증상
|
||||
- 페이지 로드 시 간헐적으로 401 에러
|
||||
- 토큰 만료 직후 첫 페이지 접속 시 로그인 페이지로 튕김
|
||||
- 콘솔에 `Token refresh failed` 로그
|
||||
|
||||
---
|
||||
|
||||
## 2. 해결 방법: 미들웨어 사전 갱신 (Pre-Refresh)
|
||||
|
||||
### 2.1 핵심 아이디어
|
||||
|
||||
**페이지 렌더링 전에 미들웨어에서 토큰을 미리 갱신**하여, 페이지 로드 시 모든 API 호출이 이미 갱신된 access_token을 사용하도록 함.
|
||||
|
||||
```
|
||||
시간 →
|
||||
────────────────────────────────────────────────────────────────────
|
||||
[브라우저 요청] → [미들웨어 7.5단계]
|
||||
↓
|
||||
access_token 없고 refresh_token만 있음?
|
||||
↓ YES
|
||||
백엔드 /api/v1/refresh 호출 (1회)
|
||||
↓
|
||||
Set-Cookie: access_token, refresh_token
|
||||
↓
|
||||
[페이지 렌더링] → auth/check, serverFetch 모두 새 access_token 사용
|
||||
↓
|
||||
✅ Race Condition 없음
|
||||
────────────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
### 2.2 기존 패턴과의 관계
|
||||
|
||||
| 기능 | 목적 | 실행 시점 | 파일 |
|
||||
|------|------|----------|------|
|
||||
| **Request Coalescing** | 동시 API 호출 시 refresh 중복 방지 | API 호출 시 401 응답 후 | `refresh-token.ts` |
|
||||
| **미들웨어 사전 갱신** | 페이지 로드 전 토큰 준비 | 미들웨어 실행 시 | `middleware.ts` |
|
||||
|
||||
두 기능은 **상호 보완적**:
|
||||
- 미들웨어가 사전 갱신하면 대부분의 경우 API 호출 시 401이 발생하지 않음
|
||||
- 만약 미들웨어 이후 토큰이 만료되면 Request Coalescing이 백업으로 동작
|
||||
|
||||
---
|
||||
|
||||
## 3. 구현 코드
|
||||
|
||||
### 3.1 파일 위치
|
||||
```
|
||||
src/middleware.ts
|
||||
```
|
||||
|
||||
### 3.2 추가된 코드 구조
|
||||
|
||||
```typescript
|
||||
// 1. 캐시 객체 (모듈 레벨)
|
||||
let middlewareRefreshCache: {
|
||||
promise: Promise<RefreshResult> | null;
|
||||
timestamp: number;
|
||||
result: RefreshResult | null;
|
||||
} = { promise: null, timestamp: 0, result: null };
|
||||
|
||||
const MIDDLEWARE_REFRESH_CACHE_TTL = 5000; // 5초
|
||||
|
||||
// 2. checkAuthentication() 확장
|
||||
function checkAuthentication(request: NextRequest): {
|
||||
isAuthenticated: boolean;
|
||||
authMode: 'sanctum' | 'bearer' | 'api-key' | null;
|
||||
needsRefresh: boolean; // 🆕 access_token 없고 refresh_token만 있음
|
||||
refreshToken: string | null; // 🆕 갱신에 사용할 토큰
|
||||
}
|
||||
|
||||
// 3. refreshTokenInMiddleware() 함수
|
||||
async function refreshTokenInMiddleware(refreshToken: string): Promise<RefreshResult>
|
||||
|
||||
// 4. middleware() 함수 내 7.5단계
|
||||
export async function middleware(request: NextRequest) {
|
||||
// ... 기존 1~7단계 ...
|
||||
|
||||
// 7.5단계: 토큰 사전 갱신
|
||||
if (needsRefresh && refreshToken) {
|
||||
const refreshResult = await refreshTokenInMiddleware(refreshToken);
|
||||
// Set-Cookie로 새 토큰 설정
|
||||
}
|
||||
|
||||
// ... 기존 8~10단계 ...
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 checkAuthentication() 반환값 변경
|
||||
|
||||
**변경 전:**
|
||||
```typescript
|
||||
return {
|
||||
isAuthenticated: boolean;
|
||||
authMode: 'sanctum' | 'bearer' | 'api-key' | null;
|
||||
}
|
||||
```
|
||||
|
||||
**변경 후:**
|
||||
```typescript
|
||||
return {
|
||||
isAuthenticated: boolean;
|
||||
authMode: 'sanctum' | 'bearer' | 'api-key' | null;
|
||||
needsRefresh: boolean; // access_token 없고 refresh_token만 있으면 true
|
||||
refreshToken: string | null; // 갱신에 사용할 refresh_token 값
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 7.5단계 사전 갱신 로직
|
||||
|
||||
```typescript
|
||||
// 7️⃣.5️⃣ 🔄 토큰 사전 갱신 (Race Condition 방지)
|
||||
if (needsRefresh && refreshToken) {
|
||||
console.log(`🔄 [Middleware] Pre-refreshing token before page render: ${pathname}`);
|
||||
|
||||
const refreshResult = await refreshTokenInMiddleware(refreshToken);
|
||||
|
||||
if (refreshResult.success && refreshResult.accessToken) {
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const intlResponse = intlMiddleware(request);
|
||||
|
||||
// Set-Cookie 헤더로 새 토큰 전송
|
||||
const accessTokenCookie = [
|
||||
`access_token=${refreshResult.accessToken}`,
|
||||
'HttpOnly',
|
||||
...(isProduction ? ['Secure'] : []),
|
||||
'SameSite=Lax',
|
||||
'Path=/',
|
||||
`Max-Age=${refreshResult.expiresIn || 7200}`,
|
||||
].join('; ');
|
||||
|
||||
const refreshTokenCookie = [
|
||||
`refresh_token=${refreshResult.refreshToken}`,
|
||||
'HttpOnly',
|
||||
...(isProduction ? ['Secure'] : []),
|
||||
'SameSite=Lax',
|
||||
'Path=/',
|
||||
'Max-Age=604800', // 7 days (하드코딩)
|
||||
].join('; ');
|
||||
|
||||
intlResponse.headers.append('Set-Cookie', accessTokenCookie);
|
||||
intlResponse.headers.append('Set-Cookie', refreshTokenCookie);
|
||||
// ... 기타 쿠키 ...
|
||||
|
||||
return intlResponse;
|
||||
} else {
|
||||
// 갱신 실패 시 로그인 페이지로
|
||||
return NextResponse.redirect(new URL('/login', request.url));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 동작 흐름도
|
||||
|
||||
### 4.1 정상 흐름 (access_token 유효)
|
||||
|
||||
```
|
||||
브라우저 → 미들웨어 → checkAuthentication()
|
||||
↓
|
||||
needsRefresh = false (access_token 있음)
|
||||
↓
|
||||
7.5단계 스킵 → 페이지 렌더링
|
||||
```
|
||||
|
||||
### 4.2 사전 갱신 흐름 (access_token 만료, refresh_token 유효)
|
||||
|
||||
```
|
||||
브라우저 → 미들웨어 → checkAuthentication()
|
||||
↓
|
||||
needsRefresh = true (access_token 없음, refresh_token 있음)
|
||||
↓
|
||||
7.5단계: refreshTokenInMiddleware() 호출
|
||||
↓
|
||||
백엔드 /api/v1/refresh → 새 토큰 발급
|
||||
↓
|
||||
Set-Cookie: access_token, refresh_token
|
||||
↓
|
||||
페이지 렌더링 (새 토큰으로)
|
||||
```
|
||||
|
||||
### 4.3 갱신 실패 흐름 (refresh_token도 만료)
|
||||
|
||||
```
|
||||
브라우저 → 미들웨어 → checkAuthentication()
|
||||
↓
|
||||
needsRefresh = true
|
||||
↓
|
||||
7.5단계: refreshTokenInMiddleware() 호출
|
||||
↓
|
||||
백엔드 → 401 (refresh_token 만료)
|
||||
↓
|
||||
redirect('/login')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 설정 값
|
||||
|
||||
| 항목 | 값 | 설명 |
|
||||
|------|-----|------|
|
||||
| MIDDLEWARE_REFRESH_CACHE_TTL | 5초 | 미들웨어 캐시 유지 시간 |
|
||||
| access_token Max-Age | 7200초 (2시간) | 백엔드 expires_in 값 또는 기본값 |
|
||||
| refresh_token Max-Age | 604800초 (7일) | 하드코딩 (백엔드에서 미제공) |
|
||||
|
||||
---
|
||||
|
||||
## 6. 로그 메시지
|
||||
|
||||
### 6.1 사전 갱신 시작
|
||||
```
|
||||
🔄 [Middleware] Pre-refreshing token before page render: /dashboard
|
||||
```
|
||||
|
||||
### 6.2 캐시 히트
|
||||
```
|
||||
🔵 [Middleware] Using cached refresh result (age: 1234ms)
|
||||
```
|
||||
|
||||
### 6.3 진행 중인 갱신 대기
|
||||
```
|
||||
🔵 [Middleware] Waiting for ongoing refresh...
|
||||
```
|
||||
|
||||
### 6.4 갱신 성공
|
||||
```
|
||||
✅ [Middleware] Pre-refresh successful
|
||||
✅ [Middleware] Pre-refresh complete, new tokens set in cookies
|
||||
```
|
||||
|
||||
### 6.5 갱신 실패
|
||||
```
|
||||
🔴 [Middleware] Pre-refresh failed: 401
|
||||
🔴 [Middleware] Pre-refresh failed, redirecting to login
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Edge Runtime 고려사항
|
||||
|
||||
### 7.1 모듈 레벨 캐시의 한계
|
||||
|
||||
Edge Runtime에서는 모듈 레벨 변수가 **요청 간 공유되지 않을 수 있음**.
|
||||
따라서 `middlewareRefreshCache`는 **같은 요청 내 중복 갱신 방지**에만 효과적.
|
||||
|
||||
### 7.2 5초 캐시의 역할
|
||||
|
||||
- 같은 요청 처리 중 여러 번 호출되는 경우 방지
|
||||
- Edge 인스턴스 간 캐시 공유는 불가능
|
||||
- 충분히 짧아서 토큰 갱신 지연 문제 없음
|
||||
|
||||
---
|
||||
|
||||
## 8. 관련 파일
|
||||
|
||||
| 파일 | 역할 |
|
||||
|------|------|
|
||||
| `src/middleware.ts` | 미들웨어 사전 갱신 로직 |
|
||||
| `src/lib/api/refresh-token.ts` | Request Coalescing 패턴 (백업) |
|
||||
| `src/app/api/auth/check/route.ts` | 인증 확인 API |
|
||||
| `src/app/api/auth/refresh/route.ts` | 토큰 갱신 프록시 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 관련 문서
|
||||
|
||||
- `[IMPL-2025-12-30] token-refresh-caching.md` - Request Coalescing 패턴 문서
|
||||
- `[IMPL-2025-11-07] middleware-issue-resolution.md` - 미들웨어 기본 구조
|
||||
|
||||
---
|
||||
|
||||
## 10. 업데이트 이력
|
||||
|
||||
### 10.1 [2026-01-15] 초기 구현
|
||||
|
||||
**배경:**
|
||||
- auth/check와 serverFetch 동시 호출 시 Race Condition 발생
|
||||
- 기존 Request Coalescing만으로는 완전히 해결되지 않음
|
||||
|
||||
**구현 내용:**
|
||||
1. `middlewareRefreshCache` 캐시 객체 추가
|
||||
2. `refreshTokenInMiddleware()` 함수 구현
|
||||
3. `checkAuthentication()`에 `needsRefresh`, `refreshToken` 반환 추가
|
||||
4. 7.5단계 사전 갱신 로직 추가
|
||||
|
||||
**결과:**
|
||||
- 페이지 렌더링 전 토큰 갱신 완료
|
||||
- 이후 API 호출들은 새 access_token 사용
|
||||
- Race Condition 완전 해결
|
||||
|
||||
### 10.2 [2026-01-15] 파편화된 API route 통합
|
||||
|
||||
**배경:**
|
||||
- `/api/menus` 등 별도 route에서 refresh 로직 없이 바로 401 반환
|
||||
- 1~2시간 방치 후 로그인 페이지로 튕기는 문제 발생
|
||||
|
||||
**수행 내용:**
|
||||
1. 클라이언트 호출 경로 변경:
|
||||
- `/api/menus` → `/api/proxy/menus` (menuRefresh.ts)
|
||||
- `/api/files/${id}/download` → `/api/proxy/files/${id}/download` (DocumentCreate, DraftBox)
|
||||
2. 파편화된 API route 삭제:
|
||||
- `src/app/api/menus/` - 삭제
|
||||
- `src/app/api/files/` - 삭제
|
||||
- `src/app/api/tenants/` - 삭제 (미사용)
|
||||
- `src/lib/api/php-proxy.ts` - 삭제 (중복 유틸)
|
||||
|
||||
**결과:**
|
||||
- 모든 API 호출이 `/api/proxy`를 통해 refresh 로직 적용
|
||||
- 토큰 만료 시 자동 갱신 후 재시도
|
||||
|
||||
### 10.3 [2026-01-15] 인증 흐름 전면 재설계
|
||||
|
||||
**배경:**
|
||||
- pre-refresh 실패 시 무한 리다이렉트 루프 발생
|
||||
- 5️⃣ 게스트 전용 라우트에서 `needsRefresh` 상태를 고려하지 않음
|
||||
- `refresh_token`만 있는 상태를 "로그인됨"으로 섣부르게 판정
|
||||
|
||||
**문제의 무한 루프 시나리오:**
|
||||
```
|
||||
/login 접근 (refresh_token만 있음)
|
||||
↓
|
||||
5️⃣ isAuthenticated=true (refresh_token 있으니까) → /dashboard로 리다이렉트
|
||||
↓
|
||||
7.5️⃣ pre-refresh 시도 → 401 실패 → /login으로 리다이렉트
|
||||
↓
|
||||
무한 반복!
|
||||
```
|
||||
|
||||
**핵심 원인:**
|
||||
- `refresh_token`만 있는 상태 = "로그인됨"이 아니라 "로그인 가능성 있음"
|
||||
- 실제로 refresh 성공해야 "진짜 로그인"
|
||||
- 5️⃣에서 이걸 확인 안 하고 바로 /dashboard로 보냄
|
||||
|
||||
**수정 내용 (5️⃣ 게스트 전용 라우트):**
|
||||
```typescript
|
||||
if (isGuestOnlyRoute(pathnameWithoutLocale)) {
|
||||
// needsRefresh인 경우: 먼저 refresh 시도해서 "진짜 로그인"인지 확인
|
||||
if (needsRefresh && refreshToken) {
|
||||
const refreshResult = await refreshTokenInMiddleware(refreshToken);
|
||||
|
||||
if (refreshResult.success) {
|
||||
// ✅ 진짜 로그인됨 → /dashboard로 (쿠키 설정)
|
||||
return redirectToDashboard(with new cookies);
|
||||
} else {
|
||||
// ❌ 로그인 안 됨 → 쿠키 삭제 후 로그인 페이지 표시 (리다이렉트 없이!)
|
||||
return showLoginPage(with cleared cookies);
|
||||
}
|
||||
}
|
||||
|
||||
// access_token 있음 = 확실히 로그인됨 → /dashboard로
|
||||
if (isAuthenticated) {
|
||||
return redirectToDashboard();
|
||||
}
|
||||
|
||||
// 쿠키 없음 = 비로그인 → 로그인 페이지 표시
|
||||
return showLoginPage();
|
||||
}
|
||||
```
|
||||
|
||||
**수정 후 흐름:**
|
||||
```
|
||||
/login 접근 (refresh_token만 있음)
|
||||
↓
|
||||
5️⃣ needsRefresh=true → refresh 먼저 시도
|
||||
↓
|
||||
├─ 성공 → "진짜 로그인" → /dashboard (왕복 1회)
|
||||
└─ 실패 → "로그인 안 됨" → 쿠키 삭제 → 로그인 페이지 (왕복 0회!)
|
||||
```
|
||||
|
||||
**결과:**
|
||||
- 무한 리다이렉트 루프 완전 해결
|
||||
- 불필요한 /dashboard → /login 왕복 제거
|
||||
- refresh 실패 시 바로 로그인 페이지 표시
|
||||
|
||||
---
|
||||
|
||||
## 11. TODO (Phase 2)
|
||||
|
||||
### 쿠키 설정 공통 모듈화
|
||||
|
||||
현재 쿠키 설정 코드가 6곳에 중복:
|
||||
- `/api/proxy/[...path]/route.ts`
|
||||
- `/api/auth/login/route.ts`
|
||||
- `/api/auth/check/route.ts`
|
||||
- `/api/auth/refresh/route.ts`
|
||||
- `middleware.ts`
|
||||
- `fetch-wrapper.ts`
|
||||
|
||||
**계획:**
|
||||
```typescript
|
||||
// src/lib/api/cookie-utils.ts (신규)
|
||||
export function createTokenCookies(tokens: TokenSet): string[]
|
||||
export function clearTokenCookies(): string[]
|
||||
```
|
||||
|
||||
**효과:** 유지보수성 향상 (쿠키 설정 변경 시 1곳만 수정)
|
||||
120
claudedocs/board/[IMPL-2025-12-30] dynamic-board-creation.md
Normal file
120
claudedocs/board/[IMPL-2025-12-30] dynamic-board-creation.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# 게시판 동적 생성 구현
|
||||
|
||||
> 작성일: 2025-12-30
|
||||
> 상태: 완료
|
||||
|
||||
## 개요
|
||||
|
||||
게시판 관리에서 게시판을 등록하면 고객센터 메뉴에 자동으로 추가되고,
|
||||
해당 게시판 페이지가 동적으로 렌더링되도록 구현합니다.
|
||||
|
||||
---
|
||||
|
||||
## 작업 목록
|
||||
|
||||
### Phase 1: 게시판 관리 폼 수정
|
||||
|
||||
- [x] 1.1 대상 옵션에 "권한" 추가
|
||||
- 현재: 전사, 부서
|
||||
- 변경: 전사, 부서, **권한**
|
||||
- 파일: `src/components/board/BoardManagement/types.ts`
|
||||
- [x] 1.2 권한 선택 시 다중 선택 체크박스 표시
|
||||
- 파일: `src/components/board/BoardManagement/BoardForm.tsx`
|
||||
- MOCK_PERMISSIONS: 관리자, 매니저, 직원, 게스트
|
||||
- [x] 1.3 API 요청 데이터에 권한 정보 포함
|
||||
- 파일: `src/components/board/BoardManagement/actions.ts`
|
||||
- transformFrontendToApi: permissions → extra_settings.permissions
|
||||
|
||||
### Phase 2: 메뉴 즉시 갱신
|
||||
|
||||
- [x] 2.1 게시판 등록 성공 후 `forceRefreshMenus()` 호출
|
||||
- 파일: `src/app/[locale]/(protected)/board/board-management/new/page.tsx`
|
||||
- [x] 2.2 게시판 수정 성공 후 `forceRefreshMenus()` 호출
|
||||
- 파일: `src/app/[locale]/(protected)/board/board-management/[id]/edit/page.tsx`
|
||||
|
||||
### Phase 3: 동적 게시판 라우트 생성
|
||||
|
||||
- [x] 3.1 `/customer-center/[boardCode]/page.tsx` - 리스트
|
||||
- [x] 3.2 `/customer-center/[boardCode]/[postId]/page.tsx` - 상세
|
||||
- [x] 3.3 `/customer-center/[boardCode]/create/page.tsx` - 등록
|
||||
- [x] 3.4 `/customer-center/[boardCode]/[postId]/edit/page.tsx` - 수정
|
||||
|
||||
### Phase 4: 테스트 및 검증
|
||||
|
||||
- [ ] 4.1 게시판 등록 → 메뉴 자동 추가 확인
|
||||
- [ ] 4.2 동적 게시판 리스트/상세/등록/수정 동작 확인
|
||||
- [ ] 4.3 권한별 접근 제어 확인
|
||||
|
||||
---
|
||||
|
||||
## 기술 명세
|
||||
|
||||
### 대상 타입
|
||||
|
||||
| 대상 | 옆 셀렉트박스 | API 필드 |
|
||||
|------|---------------|----------|
|
||||
| 전사 | 없음 | `target: 'all'` |
|
||||
| 부서 | 부서 단일 선택 | `target: 'department', target_id: number` |
|
||||
| 권한 | 권한 다중 선택 (체크박스) | `target: 'permission', permissions: string[]` |
|
||||
|
||||
### 게시판 타입
|
||||
|
||||
- **기본 타입**: 1:1문의 형태 (댓글 사용 가능)
|
||||
- **참고 페이지**: `/customer-center/qna`
|
||||
|
||||
### 메뉴 갱신 플로우
|
||||
|
||||
```
|
||||
게시판 등록 API 호출 (POST /api/v1/boards)
|
||||
↓
|
||||
백엔드: 게시판 생성 + 메뉴 테이블에 추가
|
||||
↓
|
||||
프론트: 등록 성공 응답 받음
|
||||
↓
|
||||
프론트: forceRefreshMenus() 호출
|
||||
↓
|
||||
사이드바 메뉴 즉시 업데이트
|
||||
```
|
||||
|
||||
### 동적 게시판 URL 구조
|
||||
|
||||
```
|
||||
/boards/[boardCode] → 목록
|
||||
/boards/[boardCode]/create → 등록
|
||||
/boards/[boardCode]/[postId] → 상세
|
||||
/boards/[boardCode]/[postId]/edit → 수정
|
||||
```
|
||||
|
||||
> **URL 변경 이력 (2025-12-30)**
|
||||
> - 변경 전: `/customer-center/[boardCode]`
|
||||
> - 변경 후: `/boards/[boardCode]`
|
||||
> - 사유: 백엔드 메뉴 API path 규칙에 맞춤 (`/boards/free`, `/boards/board_xxx`)
|
||||
|
||||
---
|
||||
|
||||
## 관련 파일
|
||||
|
||||
### 수정된 파일
|
||||
- `src/components/board/BoardManagement/types.ts` - BoardTarget에 'permission' 추가
|
||||
- `src/components/board/BoardManagement/BoardForm.tsx` - 권한 다중 선택 UI 추가
|
||||
- `src/components/board/BoardManagement/actions.ts` - permissions 변환 로직
|
||||
- `src/components/customer-center/shared/types.ts` - SystemBoardCode 확장
|
||||
- `src/app/[locale]/(protected)/board/board-management/new/page.tsx` - forceRefreshMenus 호출
|
||||
- `src/app/[locale]/(protected)/board/board-management/[id]/edit/page.tsx` - forceRefreshMenus 호출
|
||||
|
||||
### 새로 생성된 파일
|
||||
- `src/app/[locale]/(protected)/boards/[boardCode]/page.tsx` - 동적 게시판 목록
|
||||
- `src/app/[locale]/(protected)/boards/[boardCode]/[postId]/page.tsx` - 동적 게시판 상세
|
||||
- `src/app/[locale]/(protected)/boards/[boardCode]/create/page.tsx` - 동적 게시판 등록
|
||||
- `src/app/[locale]/(protected)/boards/[boardCode]/[postId]/edit/page.tsx` - 동적 게시판 수정
|
||||
|
||||
---
|
||||
|
||||
## 진행 로그
|
||||
|
||||
| 날짜 | 작업 내용 |
|
||||
|------|----------|
|
||||
| 2025-12-30 | 요구사항 정리 및 체크리스트 생성 |
|
||||
| 2025-12-30 | Phase 1~3 구현 완료 |
|
||||
| 2025-12-30 | URL 경로 변경: `/customer-center/[boardCode]` → `/boards/[boardCode]` |
|
||||
| 2025-12-30 | API URL 불일치 해결: `system-boards` → `boards` (DynamicBoard/actions.ts 생성) |
|
||||
@@ -0,0 +1,92 @@
|
||||
# 수주 관리 Frontend API 연동
|
||||
|
||||
**날짜:** 2025-01-08
|
||||
**Phase:** Phase 2 - Frontend 연동
|
||||
**관련 Plan:** docs/plans/order-management-plan.md
|
||||
|
||||
## 변경 개요
|
||||
|
||||
수주 관리 React 페이지들을 백엔드 API와 연동 완료. Mock 데이터를 제거하고 실제 API 호출로 대체.
|
||||
|
||||
## 수정된 파일
|
||||
|
||||
### 1. `src/components/orders/actions.ts` (신규 생성)
|
||||
- Server Actions 패턴으로 API 클라이언트 구현
|
||||
- 주요 함수:
|
||||
- `getOrders()`: 수주 목록 조회
|
||||
- `getOrderById(id)`: 수주 상세 조회
|
||||
- `createOrder(data)`: 수주 등록
|
||||
- `updateOrder(id, data)`: 수주 수정
|
||||
- `deleteOrder(id)`: 수주 삭제
|
||||
- `deleteOrders(ids)`: 수주 일괄 삭제
|
||||
- `updateOrderStatus(id, status)`: 수주 상태 변경
|
||||
- `getOrderStats()`: 통계 조회
|
||||
- 데이터 변환: API snake_case → Frontend camelCase
|
||||
- 상태 매핑: API 상태(DRAFT, CONFIRMED 등) → Frontend 상태(order_registered, order_confirmed 등)
|
||||
|
||||
### 2. `src/components/orders/index.ts` (수정)
|
||||
- actions.ts export 추가
|
||||
- 타입 충돌 해결 (OrderItem → OrderItemApi)
|
||||
|
||||
### 3. `src/app/[locale]/(protected)/sales/order-management-sales/page.tsx` (수정)
|
||||
- SAMPLE_ORDERS (~115줄) 제거
|
||||
- API 연동 state 추가: `orders`, `apiStats`, `isLoading`, `isDeleting`
|
||||
- `loadData()` 함수로 API 호출 (getOrders, getOrderStats)
|
||||
- 삭제 핸들러에 API 호출 추가 (deleteOrder, deleteOrders)
|
||||
- 로딩 UI 추가
|
||||
|
||||
### 4. `src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx` (수정)
|
||||
- SAMPLE_ITEMS, SAMPLE_ORDERS (~250줄) 제거
|
||||
- useEffect에서 getOrderById API 호출
|
||||
- handleConfirmCancel에서 updateOrderStatus API 호출
|
||||
- isCancelling 로딩 상태 적용
|
||||
|
||||
### 5. `src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx` (수정)
|
||||
- SAMPLE_ORDER (~50줄) 제거
|
||||
- useEffect에서 getOrderById API 호출
|
||||
- handleSave에서 updateOrder API 호출
|
||||
|
||||
### 6. `src/app/[locale]/(protected)/sales/order-management-sales/new/page.tsx` (수정)
|
||||
- handleSave에서 createOrder API 호출
|
||||
|
||||
## 기술 패턴
|
||||
|
||||
### Server Actions 패턴
|
||||
```typescript
|
||||
"use server";
|
||||
import { serverFetch } from "@/lib/api/serverFetch";
|
||||
|
||||
export async function getOrders() {
|
||||
const response = await serverFetch("/orders");
|
||||
// 데이터 변환 로직
|
||||
}
|
||||
```
|
||||
|
||||
### 데이터 변환
|
||||
- API: `order_no`, `client_name`, `site_name`
|
||||
- Frontend: `orderNo`, `clientName`, `siteName`
|
||||
|
||||
### 상태 매핑
|
||||
| API | Frontend |
|
||||
|-----|----------|
|
||||
| DRAFT | order_registered |
|
||||
| CONFIRMED | order_confirmed |
|
||||
| IN_PROGRESS | production_ordered |
|
||||
| COMPLETED | shipped |
|
||||
| CANCELLED | cancelled |
|
||||
|
||||
## 테스트 체크리스트
|
||||
|
||||
- [ ] 수주 목록 로드
|
||||
- [ ] 수주 상세 조회
|
||||
- [ ] 수주 등록 (견적 선택 후)
|
||||
- [ ] 수주 수정
|
||||
- [ ] 수주 개별 삭제
|
||||
- [ ] 수주 일괄 삭제
|
||||
- [ ] 수주 취소
|
||||
- [ ] 통계 카드 표시
|
||||
|
||||
## 연관 작업
|
||||
|
||||
- Phase 1: Order API 백엔드 구현 (커밋: de19ac9)
|
||||
- Phase 1.1: OrderController/Service 구현 (진행 중)
|
||||
113
claudedocs/changes/20250108_order_phase3_advanced_features.md
Normal file
113
claudedocs/changes/20250108_order_phase3_advanced_features.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# 수주 관리 Phase 3 - 고급 기능
|
||||
|
||||
**날짜:** 2025-01-08
|
||||
**Phase:** Phase 3 - 고급 기능
|
||||
**관련 Plan:** docs/plans/order-management-plan.md
|
||||
|
||||
## 변경 개요
|
||||
|
||||
수주 관리 시스템에 견적→수주 변환 및 생산지시 생성 기능 추가.
|
||||
|
||||
## API 추가 사항
|
||||
|
||||
### 1. 견적에서 수주 생성
|
||||
- **Endpoint**: `POST /api/v1/orders/from-quote/{quoteId}`
|
||||
- **기능**: 기존 견적서를 기반으로 수주를 자동 생성
|
||||
- **검증**: 이미 수주가 생성된 견적은 중복 생성 방지
|
||||
|
||||
### 2. 생산지시 생성
|
||||
- **Endpoint**: `POST /api/v1/orders/{id}/production-order`
|
||||
- **기능**: 확정된 수주에서 작업지시(WorkOrder) 생성
|
||||
- **검증**: CONFIRMED 상태의 수주만 생산지시 가능
|
||||
|
||||
## 수정된 파일
|
||||
|
||||
### API (Laravel)
|
||||
|
||||
#### 1. `app/Services/OrderService.php`
|
||||
- `createFromQuote(int $quoteId, array $data)`: 견적→수주 변환 로직
|
||||
- `createProductionOrder(int $orderId, array $data)`: 생산지시 생성 로직
|
||||
- `generateWorkOrderNo(int $tenantId)`: 작업지시번호 자동 생성
|
||||
|
||||
#### 2. `app/Http/Controllers/Api/V1/OrderController.php`
|
||||
- `createFromQuote()`: 견적→수주 액션
|
||||
- `createProductionOrder()`: 생산지시 생성 액션
|
||||
|
||||
#### 3. `app/Http/Requests/Order/CreateFromQuoteRequest.php` (신규)
|
||||
- 견적→수주 변환 요청 검증
|
||||
- 선택 필드: delivery_date, memo
|
||||
|
||||
#### 4. `app/Http/Requests/Order/CreateProductionOrderRequest.php` (신규)
|
||||
- 생산지시 생성 요청 검증
|
||||
- 선택 필드: process_type, assignee_id, team_id, scheduled_date, memo
|
||||
|
||||
#### 5. `routes/api.php`
|
||||
- `POST /orders/from-quote/{quoteId}`: 견적→수주 라우트
|
||||
- `POST /orders/{id}/production-order`: 생산지시 라우트
|
||||
|
||||
#### 6. `lang/ko/message.php`
|
||||
- `order.created_from_quote`: 견적에서 수주가 생성되었습니다.
|
||||
- `order.production_order_created`: 생산지시가 생성되었습니다.
|
||||
|
||||
#### 7. `lang/ko/error.php`
|
||||
- `order.already_created_from_quote`: 이미 해당 견적에서 수주가 생성되었습니다.
|
||||
- `order.must_be_confirmed_for_production`: 확정 상태의 수주만 생산지시를 생성할 수 있습니다.
|
||||
- `order.production_order_already_exists`: 이미 생산지시가 존재합니다.
|
||||
- `quote.not_found`: 견적을 찾을 수 없습니다.
|
||||
|
||||
### Frontend (React)
|
||||
|
||||
#### 1. `src/components/orders/actions.ts`
|
||||
- 타입 추가: `CreateFromQuoteData`, `CreateProductionOrderData`, `WorkOrder`, `ProductionOrderResult`
|
||||
- API 인터페이스 추가: `ApiWorkOrder`, `ApiProductionOrderResponse`
|
||||
- `createOrderFromQuote(quoteId, data)`: 견적→수주 API 호출
|
||||
- `createProductionOrder(orderId, data)`: 생산지시 생성 API 호출
|
||||
- `transformWorkOrderApiToFrontend()`: WorkOrder 데이터 변환
|
||||
|
||||
## 비즈니스 로직
|
||||
|
||||
### 견적→수주 변환 흐름
|
||||
```
|
||||
Quote (견적)
|
||||
↓ createFromQuote()
|
||||
Order (수주) - DRAFT 상태
|
||||
- quote_id 연결
|
||||
- client, site_name 복사
|
||||
- items 변환 (quantity=calculated_quantity)
|
||||
- 금액 재계산
|
||||
```
|
||||
|
||||
### 생산지시 생성 흐름
|
||||
```
|
||||
Order (수주) - CONFIRMED 상태
|
||||
↓ createProductionOrder()
|
||||
WorkOrder (작업지시) - PENDING 상태
|
||||
- sales_order_id 연결
|
||||
- project_name = site_name
|
||||
- process_type 설정
|
||||
↓
|
||||
Order 상태 → IN_PROGRESS
|
||||
```
|
||||
|
||||
### 상태 전환 규칙 (기존)
|
||||
```
|
||||
DRAFT → CONFIRMED → IN_PROGRESS → COMPLETED
|
||||
↓ ↓ ↓
|
||||
CANCELLED (어느 단계에서든 취소 가능)
|
||||
```
|
||||
|
||||
## 테스트 체크리스트
|
||||
|
||||
- [ ] 견적→수주 생성 (정상 케이스)
|
||||
- [ ] 견적→수주 생성 (중복 방지)
|
||||
- [ ] 견적→수주 생성 (존재하지 않는 견적)
|
||||
- [ ] 생산지시 생성 (정상 케이스)
|
||||
- [ ] 생산지시 생성 (CONFIRMED 아닌 수주)
|
||||
- [ ] 생산지시 생성 (중복 방지)
|
||||
- [ ] 수주 상태 자동 변경 (CONFIRMED → IN_PROGRESS)
|
||||
|
||||
## 연관 작업
|
||||
|
||||
- Phase 1: Order API 백엔드 구현 (커밋: de19ac9)
|
||||
- Phase 2: Frontend API 연동 (커밋: 572ffe8)
|
||||
- Phase 3: 고급 기능 (현재)
|
||||
691
claudedocs/components/_registry.md
Normal file
691
claudedocs/components/_registry.md
Normal file
@@ -0,0 +1,691 @@
|
||||
# Component Registry
|
||||
|
||||
> Auto-generated: 2026-02-12T01:56:50.520Z
|
||||
> Total: **501** components
|
||||
|
||||
## UI (53)
|
||||
|
||||
### ui (53)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| Accordion | accordion.tsx | none | | Y | 66 |
|
||||
| AccountNumberInput | account-number-input.tsx | none | AccountNumberInputProps | Y | 95 |
|
||||
| Alert | alert.tsx | none | VariantProps | | 59 |
|
||||
| AlertDialog | alert-dialog.tsx | none | | Y | 158 |
|
||||
| Badge | badge.tsx | none | VariantProps | | 47 |
|
||||
| BusinessNumberInput | business-number-input.tsx | none | BusinessNumberInputProps | Y | 114 |
|
||||
| Button | button.tsx | none | VariantProps | | 62 |
|
||||
| Calendar | calendar.tsx | none | | Y | 138 |
|
||||
| Card | card.tsx | none | | | 93 |
|
||||
| CardNumberInput | card-number-input.tsx | none | CardNumberInputProps | Y | 95 |
|
||||
| ChartWrapper | chart-wrapper.tsx | named | ChartWrapperProps | | 66 |
|
||||
| Checkbox | checkbox.tsx | none | | Y | 33 |
|
||||
| Collapsible | collapsible.tsx | none | | Y | 33 |
|
||||
| Command | command.tsx | none | | Y | 177 |
|
||||
| ConfirmDialog | confirm-dialog.tsx | both | ConfirmDialogProps | Y | 226 |
|
||||
| CurrencyInput | currency-input.tsx | none | CurrencyInputProps | Y | 220 |
|
||||
| DatePicker | date-picker.tsx | none | DatePickerProps | Y | 279 |
|
||||
| Dialog | dialog.tsx | none | | Y | 137 |
|
||||
| Drawer | drawer.tsx | none | | Y | 133 |
|
||||
| DropdownMenu | dropdown-menu.tsx | none | | Y | 258 |
|
||||
| EmptyState | empty-state.tsx | both | ButtonProps | Y | 227 |
|
||||
| ErrorCard | error-card.tsx | named | ErrorCardProps | Y | 196 |
|
||||
| ErrorMessage | error-message.tsx | named | ErrorMessageProps | | 38 |
|
||||
| FileDropzone | file-dropzone.tsx | both | FileDropzoneProps | Y | 227 |
|
||||
| FileInput | file-input.tsx | both | FileInputProps | Y | 226 |
|
||||
| FileList | file-list.tsx | both | FileListProps | Y | 276 |
|
||||
| ImageUpload | image-upload.tsx | both | ImageUploadProps | Y | 309 |
|
||||
| Input | input.tsx | none | | | 22 |
|
||||
| Label | label.tsx | none | | Y | 25 |
|
||||
| LoadingSpinner | loading-spinner.tsx | named | LoadingSpinnerProps | | 114 |
|
||||
| MultiSelectCombobox | multi-select-combobox.tsx | named | MultiSelectComboboxProps | Y | 128 |
|
||||
| NumberInput | number-input.tsx | none | NumberInputProps | Y | 280 |
|
||||
| PersonalNumberInput | personal-number-input.tsx | none | PersonalNumberInputProps | Y | 101 |
|
||||
| PhoneInput | phone-input.tsx | none | PhoneInputProps | Y | 95 |
|
||||
| Popover | popover.tsx | none | | Y | 53 |
|
||||
| Progress | progress.tsx | none | | Y | 32 |
|
||||
| QuantityInput | quantity-input.tsx | none | QuantityInputProps | Y | 271 |
|
||||
| RadioGroup | radio-group.tsx | none | | Y | 46 |
|
||||
| ScrollArea | scroll-area.tsx | none | | Y | 53 |
|
||||
| SearchableSelect | searchable-select.tsx | named | SearchableSelectProps | Y | 219 |
|
||||
| Select | select.tsx | none | | Y | 192 |
|
||||
| Separator | separator.tsx | none | SeparatorProps | Y | 32 |
|
||||
| Sheet | sheet.tsx | none | | Y | 146 |
|
||||
| Skeleton | skeleton.tsx | none | SkeletonProps | Y | 679 |
|
||||
| Slider | slider.tsx | none | | | 26 |
|
||||
| StatusBadge | status-badge.tsx | both | StatusBadgeProps | Y | 123 |
|
||||
| Switch | switch.tsx | none | | Y | 32 |
|
||||
| Table | table.tsx | none | | | 117 |
|
||||
| Tabs | tabs.tsx | none | | Y | 66 |
|
||||
| Textarea | textarea.tsx | none | | | 25 |
|
||||
| TimePicker | time-picker.tsx | none | TimePickerProps | Y | 191 |
|
||||
| Tooltip | tooltip.tsx | none | | Y | 48 |
|
||||
| VisuallyHidden | visually-hidden.tsx | none | | Y | 14 |
|
||||
|
||||
## ATOMS (3)
|
||||
|
||||
### atoms (3)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| BadgeSm | BadgeSm.tsx | named | BadgeSmProps | Y | 117 |
|
||||
| ScrollableButtonGroup | ScrollableButtonGroup.tsx | named | ScrollableButtonGroupProps | | 53 |
|
||||
| TabChip | TabChip.tsx | named | TabChipProps | Y | 72 |
|
||||
|
||||
## MOLECULES (8)
|
||||
|
||||
### molecules (8)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| DateRangeSelector | DateRangeSelector.tsx | named | DateRangeSelectorProps | Y | 217 |
|
||||
| FormField | FormField.tsx | named | FormFieldProps | | 296 |
|
||||
| IconWithBadge | IconWithBadge.tsx | named | IconWithBadgeProps | Y | 51 |
|
||||
| MobileFilter | MobileFilter.tsx | named | MobileFilterProps | Y | 335 |
|
||||
| StandardDialog | StandardDialog.tsx | named | StandardDialogProps | Y | 219 |
|
||||
| StatusBadge | StatusBadge.tsx | named | StatusBadgeProps | Y | 111 |
|
||||
| TableActions | TableActions.tsx | named | TableActionsProps | Y | 89 |
|
||||
| YearQuarterFilter | YearQuarterFilter.tsx | named | YearQuarterFilterProps | Y | 98 |
|
||||
|
||||
## ORGANISMS (12)
|
||||
|
||||
### organisms (12)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| DataTable | DataTable.tsx | named | DataTableProps | Y | 363 |
|
||||
| EmptyState | EmptyState.tsx | named | EmptyStateProps | Y | 38 |
|
||||
| FormActions | FormActions.tsx | named | FormActionsProps | | 74 |
|
||||
| FormFieldGrid | FormFieldGrid.tsx | named | FormFieldGridProps | | 35 |
|
||||
| FormSection | FormSection.tsx | named | FormSectionProps | | 62 |
|
||||
| MobileCard | MobileCard.tsx | named | InfoFieldProps | Y | 347 |
|
||||
| PageHeader | PageHeader.tsx | named | PageHeaderProps | Y | 42 |
|
||||
| PageLayout | PageLayout.tsx | named | PageLayoutProps | Y | 32 |
|
||||
| ScreenVersionHistory | ScreenVersionHistory.tsx | named | ScreenVersionHistoryProps | Y | 75 |
|
||||
| SearchableSelectionModal | SearchableSelectionModal.tsx | named | | Y | 253 |
|
||||
| SearchFilter | SearchFilter.tsx | named | SearchFilterProps | Y | 58 |
|
||||
| StatCards | StatCards.tsx | named | StatCardsProps | Y | 67 |
|
||||
|
||||
## COMMON (16)
|
||||
|
||||
### common (16)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| AccessDenied | AccessDenied.tsx | named | AccessDeniedProps | Y | 68 |
|
||||
| CalendarHeader | CalendarHeader.tsx | named | | Y | 148 |
|
||||
| DayCell | DayCell.tsx | named | DayCellProps | Y | 93 |
|
||||
| DayTimeView | DayTimeView.tsx | named | | Y | 167 |
|
||||
| EditableTable | EditableTable.tsx | both | EditableTableProps | Y | 333 |
|
||||
| EmptyPage | EmptyPage.tsx | named | EmptyPageProps | Y | 150 |
|
||||
| MonthView | MonthView.tsx | named | WeekRowProps | Y | 264 |
|
||||
| MorePopover | MorePopover.tsx | named | MorePopoverProps | Y | 45 |
|
||||
| NoticePopupModal | NoticePopupModal.tsx | named | NoticePopupModalProps | Y | 171 |
|
||||
| ParentMenuRedirect | ParentMenuRedirect.tsx | named | ParentMenuRedirectProps | Y | 82 |
|
||||
| PermissionGuard | PermissionGuard.tsx | named | PermissionGuardProps | Y | 44 |
|
||||
| ScheduleBar | ScheduleBar.tsx | named | ScheduleBarProps | Y | 96 |
|
||||
| ScheduleCalendar | ScheduleCalendar.tsx | named | | Y | 194 |
|
||||
| ServerErrorPage | ServerErrorPage.tsx | named | ServerErrorPageProps | Y | 140 |
|
||||
| WeekTimeView | WeekTimeView.tsx | named | | Y | 217 |
|
||||
| WeekView | WeekView.tsx | named | | Y | 211 |
|
||||
|
||||
## LAYOUT (3)
|
||||
|
||||
### layout (3)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| CommandMenuSearch | CommandMenuSearch.tsx | default | | Y | 199 |
|
||||
| HeaderFavoritesBar | HeaderFavoritesBar.tsx | default | HeaderFavoritesBarProps | Y | 156 |
|
||||
| Sidebar | Sidebar.tsx | default | SidebarProps | | 390 |
|
||||
|
||||
## DEV (2)
|
||||
|
||||
### dev (2)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| DevFillProvider | DevFillContext.tsx | named | DevFillProviderProps | Y | 179 |
|
||||
| DevToolbar | DevToolbar.tsx | both | | Y | 499 |
|
||||
|
||||
## DOMAIN (404)
|
||||
|
||||
### LanguageSelect.tsx (1)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| LanguageSelect | LanguageSelect.tsx | named | LanguageSelectProps | Y | 90 |
|
||||
|
||||
### ThemeSelect.tsx (1)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| ThemeSelect | ThemeSelect.tsx | named | ThemeSelectProps | Y | 82 |
|
||||
|
||||
### accounting (19)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| BadDebtDetail | BadDebtDetail.tsx | named | BadDebtDetailProps | Y | 963 |
|
||||
| BadDebtDetailClientV2 | BadDebtDetailClientV2.tsx | named | BadDebtDetailClientV2Props | Y | 136 |
|
||||
| BillDetail | BillDetail.tsx | named | BillDetailProps | Y | 540 |
|
||||
| BillManagementClient | BillManagementClient.tsx | named | BillManagementClientProps | Y | 522 |
|
||||
| CardTransactionDetailClient | CardTransactionDetailClient.tsx | default | CardTransactionDetailClientProps | Y | 139 |
|
||||
| CreditAnalysisDocument | CreditAnalysisDocument.tsx | named | CreditAnalysisDocumentProps | Y | 210 |
|
||||
| CreditSignal | CreditSignal.tsx | named | CreditSignalProps | Y | 58 |
|
||||
| DepositDetail | DepositDetail.tsx | named | DepositDetailProps | Y | 327 |
|
||||
| DepositDetailClientV2 | DepositDetailClientV2.tsx | default | DepositDetailClientV2Props | Y | 144 |
|
||||
| PurchaseDetail | PurchaseDetail.tsx | named | PurchaseDetailProps | Y | 697 |
|
||||
| PurchaseDetailModal | PurchaseDetailModal.tsx | named | PurchaseDetailModalProps | Y | 402 |
|
||||
| RiskRadarChart | RiskRadarChart.tsx | named | RiskRadarChartProps | Y | 95 |
|
||||
| SalesDetail | SalesDetail.tsx | named | SalesDetailProps | Y | 579 |
|
||||
| VendorDetail | VendorDetail.tsx | named | VendorDetailProps | Y | 684 |
|
||||
| VendorDetailClient | VendorDetailClient.tsx | named | VendorDetailClientProps | Y | 586 |
|
||||
| VendorLedgerDetail | VendorLedgerDetail.tsx | named | VendorLedgerDetailProps | Y | 386 |
|
||||
| VendorManagementClient | VendorManagementClient.tsx | named | VendorManagementClientProps | Y | 574 |
|
||||
| WithdrawalDetail | WithdrawalDetail.tsx | named | WithdrawalDetailProps | Y | 327 |
|
||||
| WithdrawalDetailClientV2 | WithdrawalDetailClientV2.tsx | default | WithdrawalDetailClientV2Props | Y | 144 |
|
||||
|
||||
### approval (11)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| ApprovalLineBox | ApprovalLineBox.tsx | named | ApprovalLineBoxProps | Y | 85 |
|
||||
| ApprovalLineSection | ApprovalLineSection.tsx | named | ApprovalLineSectionProps | Y | 108 |
|
||||
| BasicInfoSection | BasicInfoSection.tsx | named | BasicInfoSectionProps | Y | 81 |
|
||||
| DocumentDetailModalV2 | DocumentDetailModalV2.tsx | named | | Y | 94 |
|
||||
| ExpenseEstimateDocument | ExpenseEstimateDocument.tsx | named | ExpenseEstimateDocumentProps | Y | 130 |
|
||||
| ExpenseEstimateForm | ExpenseEstimateForm.tsx | named | ExpenseEstimateFormProps | Y | 167 |
|
||||
| ExpenseReportDocument | ExpenseReportDocument.tsx | named | ExpenseReportDocumentProps | Y | 138 |
|
||||
| ExpenseReportForm | ExpenseReportForm.tsx | named | ExpenseReportFormProps | Y | 243 |
|
||||
| ProposalDocument | ProposalDocument.tsx | named | ProposalDocumentProps | Y | 117 |
|
||||
| ProposalForm | ProposalForm.tsx | named | ProposalFormProps | Y | 234 |
|
||||
| ReferenceSection | ReferenceSection.tsx | named | ReferenceSectionProps | Y | 109 |
|
||||
|
||||
### attendance (2)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| AttendanceComplete | AttendanceComplete.tsx | default | AttendanceCompleteProps | Y | 83 |
|
||||
| GoogleMap | GoogleMap.tsx | default | GoogleMapProps | Y | 309 |
|
||||
|
||||
### auth (2)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| LoginPage | LoginPage.tsx | named | | Y | 301 |
|
||||
| SignupPage | SignupPage.tsx | named | | Y | 763 |
|
||||
|
||||
### board (8)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| BoardDetail | BoardDetail.tsx | named | BoardDetailProps | Y | 120 |
|
||||
| BoardDetailClientV2 | BoardDetailClientV2.tsx | named | BoardDetailClientV2Props | Y | 308 |
|
||||
| BoardForm | BoardForm.tsx | named | BoardFormProps | Y | 271 |
|
||||
| BoardListUnified | BoardListUnified.tsx | both | | Y | 372 |
|
||||
| CommentItem | CommentItem.tsx | both | CommentItemProps | Y | 161 |
|
||||
| DynamicBoardCreateForm | DynamicBoardCreateForm.tsx | named | DynamicBoardCreateFormProps | Y | 166 |
|
||||
| DynamicBoardEditForm | DynamicBoardEditForm.tsx | named | DynamicBoardEditFormProps | Y | 253 |
|
||||
| MenuBar | MenuBar.tsx | named | MenuBarProps | Y | 289 |
|
||||
|
||||
### business (97)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| BiddingDetailForm | BiddingDetailForm.tsx | default | BiddingDetailFormProps | Y | 533 |
|
||||
| BiddingListClient | BiddingListClient.tsx | default | BiddingListClientProps | Y | 385 |
|
||||
| CalendarSection | CalendarSection.tsx | named | CalendarSectionProps | Y | 421 |
|
||||
| CardManagementSection | CardManagementSection.tsx | named | CardManagementSectionProps | Y | 71 |
|
||||
| CategoryDialog | CategoryDialog.tsx | named | | Y | 89 |
|
||||
| CEODashboard | CEODashboard.tsx | named | | Y | 407 |
|
||||
| ConstructionDashboard | ConstructionDashboard.tsx | named | | Y | 19 |
|
||||
| ConstructionDetailCard | ConstructionDetailCard.tsx | named | ConstructionDetailCardProps | Y | 83 |
|
||||
| ConstructionDetailClient | ConstructionDetailClient.tsx | default | ConstructionDetailClientProps | Y | 732 |
|
||||
| ConstructionMainDashboard | ConstructionMainDashboard.tsx | named | | Y | 196 |
|
||||
| ConstructionManagementListClient | ConstructionManagementListClient.tsx | default | ConstructionManagementListClientProps | Y | 540 |
|
||||
| ContractDetailForm | ContractDetailForm.tsx | default | ContractDetailFormProps | Y | 541 |
|
||||
| ContractDocumentModal | ContractDocumentModal.tsx | named | ContractDocumentModalProps | Y | 64 |
|
||||
| ContractInfoCard | ContractInfoCard.tsx | named | ContractInfoCardProps | Y | 169 |
|
||||
| ContractInfoCard | ContractInfoCard.tsx | named | ContractInfoCardProps | Y | 58 |
|
||||
| ContractListClient | ContractListClient.tsx | default | ContractListClientProps | Y | 399 |
|
||||
| DailyReportSection | DailyReportSection.tsx | named | DailyReportSectionProps | Y | 37 |
|
||||
| Dashboard | Dashboard.tsx | named | | Y | 33 |
|
||||
| DashboardSettingsDialog | DashboardSettingsDialog.tsx | named | DashboardSettingsDialogProps | Y | 744 |
|
||||
| DashboardSwitcher | DashboardSwitcher.tsx | named | | Y | 89 |
|
||||
| DebtCollectionSection | DebtCollectionSection.tsx | named | DebtCollectionSectionProps | Y | 62 |
|
||||
| DetailAccordion | DetailAccordion.tsx | default | DetailAccordionProps | Y | 199 |
|
||||
| DetailCard | DetailCard.tsx | default | DetailCardProps | Y | 69 |
|
||||
| DetailModal | DetailModal.tsx | named | DetailModalProps | Y | 763 |
|
||||
| DirectConstructionContent | DirectConstructionContent.tsx | named | DirectConstructionContentProps | Y | 157 |
|
||||
| DirectConstructionModal | DirectConstructionModal.tsx | named | DirectConstructionModalProps | Y | 60 |
|
||||
| ElectronicApprovalModal | ElectronicApprovalModal.tsx | named | ElectronicApprovalModalProps | Y | 299 |
|
||||
| ElectronicApprovalModal | ElectronicApprovalModal.tsx | none | | | 2 |
|
||||
| EnhancedDailyReportSection | EnhancedSections.tsx | named | EnhancedDailyReportSectionProps | Y | 534 |
|
||||
| EntertainmentSection | EntertainmentSection.tsx | named | EntertainmentSectionProps | Y | 53 |
|
||||
| EstimateDetailForm | EstimateDetailForm.tsx | default | EstimateDetailFormProps | Y | 761 |
|
||||
| EstimateDetailTableSection | EstimateDetailTableSection.tsx | named | EstimateDetailTableSectionProps | Y | 657 |
|
||||
| EstimateDocumentContent | EstimateDocumentContent.tsx | named | EstimateDocumentContentProps | Y | 286 |
|
||||
| EstimateDocumentModal | EstimateDocumentModal.tsx | named | EstimateDocumentModalProps | Y | 88 |
|
||||
| EstimateInfoSection | EstimateInfoSection.tsx | named | EstimateInfoSectionProps | Y | 262 |
|
||||
| EstimateListClient | EstimateListClient.tsx | default | EstimateListClientProps | Y | 376 |
|
||||
| EstimateSummarySection | EstimateSummarySection.tsx | named | EstimateSummarySectionProps | Y | 182 |
|
||||
| ExpenseDetailSection | ExpenseDetailSection.tsx | named | ExpenseDetailSectionProps | Y | 197 |
|
||||
| HandoverReportDetailForm | HandoverReportDetailForm.tsx | default | HandoverReportDetailFormProps | Y | 694 |
|
||||
| HandoverReportDocumentModal | HandoverReportDocumentModal.tsx | named | HandoverReportDocumentModalProps | Y | 236 |
|
||||
| HandoverReportListClient | HandoverReportListClient.tsx | default | HandoverReportListClientProps | Y | 387 |
|
||||
| IndirectConstructionContent | IndirectConstructionContent.tsx | named | IndirectConstructionContentProps | Y | 143 |
|
||||
| IndirectConstructionModal | IndirectConstructionModal.tsx | named | IndirectConstructionModalProps | Y | 60 |
|
||||
| IssueDetailForm | IssueDetailForm.tsx | default | IssueDetailFormProps | Y | 625 |
|
||||
| IssueManagementListClient | IssueManagementListClient.tsx | default | IssueManagementListClientProps | Y | 514 |
|
||||
| ItemDetailClient | ItemDetailClient.tsx | default | ItemDetailClientProps | Y | 487 |
|
||||
| ItemManagementClient | ItemManagementClient.tsx | default | ItemManagementClientProps | Y | 618 |
|
||||
| KanbanColumn | KanbanColumn.tsx | default | KanbanColumnProps | Y | 53 |
|
||||
| LaborDetailClient | LaborDetailClient.tsx | default | LaborDetailClientProps | Y | 121 |
|
||||
| LaborManagementClient | LaborManagementClient.tsx | default | LaborManagementClientProps | Y | 372 |
|
||||
| MainDashboard | MainDashboard.tsx | named | | | 2652 |
|
||||
| MonthlyExpenseSection | MonthlyExpenseSection.tsx | named | MonthlyExpenseSectionProps | Y | 38 |
|
||||
| OrderDetailForm | OrderDetailForm.tsx | default | OrderDetailFormProps | Y | 276 |
|
||||
| OrderDetailItemTable | OrderDetailItemTable.tsx | named | OrderDetailItemTableProps | Y | 445 |
|
||||
| OrderDialogs | OrderDialogs.tsx | named | OrderDialogsProps | Y | 66 |
|
||||
| OrderDocumentModal | OrderDocumentModal.tsx | named | OrderDocumentModalProps | Y | 311 |
|
||||
| OrderInfoCard | OrderInfoCard.tsx | named | OrderInfoCardProps | Y | 143 |
|
||||
| OrderManagementListClient | OrderManagementListClient.tsx | default | OrderManagementListClientProps | Y | 608 |
|
||||
| OrderManagementUnified | OrderManagementUnified.tsx | both | OrderManagementUnifiedProps | Y | 641 |
|
||||
| OrderMemoCard | OrderMemoCard.tsx | named | OrderMemoCardProps | Y | 29 |
|
||||
| OrderScheduleCard | OrderScheduleCard.tsx | named | OrderScheduleCardProps | Y | 42 |
|
||||
| PartnerForm | PartnerForm.tsx | default | PartnerFormProps | Y | 642 |
|
||||
| PartnerListClient | PartnerListClient.tsx | default | PartnerListClientProps | Y | 335 |
|
||||
| PhotoDocumentContent | PhotoDocumentContent.tsx | named | PhotoDocumentContentProps | Y | 130 |
|
||||
| PhotoDocumentModal | PhotoDocumentModal.tsx | named | PhotoDocumentModalProps | Y | 60 |
|
||||
| PhotoTable | PhotoTable.tsx | named | PhotoTableProps | Y | 153 |
|
||||
| PriceAdjustmentSection | PriceAdjustmentSection.tsx | named | PriceAdjustmentSectionProps | Y | 150 |
|
||||
| PricingDetailClient | PricingDetailClient.tsx | default | PricingDetailClientProps | Y | 135 |
|
||||
| PricingListClient | PricingListClient.tsx | default | PricingListClientProps | Y | 477 |
|
||||
| ProgressBillingDetailForm | ProgressBillingDetailForm.tsx | default | ProgressBillingDetailFormProps | Y | 193 |
|
||||
| ProgressBillingInfoCard | ProgressBillingInfoCard.tsx | named | ProgressBillingInfoCardProps | Y | 78 |
|
||||
| ProgressBillingItemTable | ProgressBillingItemTable.tsx | named | ProgressBillingItemTableProps | Y | 193 |
|
||||
| ProgressBillingManagementListClient | ProgressBillingManagementListClient.tsx | default | ProgressBillingManagementListClientProps | Y | 343 |
|
||||
| ProjectCard | ProjectCard.tsx | default | ProjectCardProps | Y | 89 |
|
||||
| ProjectDetailClient | ProjectDetailClient.tsx | default | ProjectDetailClientProps | Y | 197 |
|
||||
| ProjectEndDialog | ProjectEndDialog.tsx | default | ProjectEndDialogProps | Y | 192 |
|
||||
| ProjectGanttChart | ProjectGanttChart.tsx | default | ProjectGanttChartProps | Y | 367 |
|
||||
| ProjectKanbanBoard | ProjectKanbanBoard.tsx | default | ProjectKanbanBoardProps | Y | 244 |
|
||||
| ProjectListClient | ProjectListClient.tsx | default | ProjectListClientProps | Y | 629 |
|
||||
| ReceivableSection | ReceivableSection.tsx | named | ReceivableSectionProps | Y | 69 |
|
||||
| ScheduleDetailModal | ScheduleDetailModal.tsx | named | ScheduleDetailModalProps | Y | 290 |
|
||||
| SECTION_THEME_STYLES | components.tsx | named | | Y | 434 |
|
||||
| SiteBriefingForm | SiteBriefingForm.tsx | default | SiteBriefingFormProps | Y | 957 |
|
||||
| SiteBriefingListClient | SiteBriefingListClient.tsx | default | SiteBriefingListClientProps | Y | 362 |
|
||||
| SiteDetailClientV2 | SiteDetailClientV2.tsx | both | SiteDetailClientV2Props | Y | 141 |
|
||||
| SiteDetailForm | SiteDetailForm.tsx | default | SiteDetailFormProps | Y | 386 |
|
||||
| SiteManagementListClient | SiteManagementListClient.tsx | default | SiteManagementListClientProps | Y | 338 |
|
||||
| StageCard | StageCard.tsx | default | StageCardProps | Y | 89 |
|
||||
| StatusBoardSection | StatusBoardSection.tsx | named | StatusBoardSectionProps | Y | 72 |
|
||||
| StructureReviewDetailClientV2 | StructureReviewDetailClientV2.tsx | both | StructureReviewDetailClientV2Props | Y | 149 |
|
||||
| StructureReviewDetailForm | StructureReviewDetailForm.tsx | default | StructureReviewDetailFormProps | Y | 390 |
|
||||
| StructureReviewListClient | StructureReviewListClient.tsx | default | StructureReviewListClientProps | Y | 375 |
|
||||
| TodayIssueSection | TodayIssueSection.tsx | named | TodayIssueSectionProps | Y | 453 |
|
||||
| UtilityManagementListClient | UtilityManagementListClient.tsx | default | UtilityManagementListClientProps | Y | 395 |
|
||||
| VatSection | VatSection.tsx | named | VatSectionProps | Y | 38 |
|
||||
| WelfareSection | WelfareSection.tsx | named | WelfareSectionProps | Y | 53 |
|
||||
| WorkerStatusListClient | WorkerStatusListClient.tsx | default | WorkerStatusListClientProps | Y | 416 |
|
||||
|
||||
### checklist-management (7)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| ChecklistDetail | ChecklistDetail.tsx | named | ChecklistDetailProps | Y | 316 |
|
||||
| ChecklistDetailClient | ChecklistDetailClient.tsx | named | ChecklistDetailClientProps | Y | 123 |
|
||||
| ChecklistForm | ChecklistForm.tsx | named | ChecklistFormProps | Y | 173 |
|
||||
| ChecklistListClient | ChecklistListClient.tsx | default | | Y | 520 |
|
||||
| ItemDetail | ItemDetail.tsx | named | ItemDetailProps | Y | 224 |
|
||||
| ItemDetailClient | ItemDetailClient.tsx | named | ItemDetailClientProps | Y | 111 |
|
||||
| ItemForm | ItemForm.tsx | named | ItemFormProps | Y | 351 |
|
||||
|
||||
### clients (3)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| ClientDetail | ClientDetail.tsx | named | ClientDetailProps | Y | 254 |
|
||||
| ClientDetailClientV2 | ClientDetailClientV2.tsx | named | ClientDetailClientV2Props | Y | 253 |
|
||||
| ClientRegistration | ClientRegistration.tsx | named | ClientRegistrationProps | Y | 468 |
|
||||
|
||||
### customer-center (9)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| EventDetail | EventDetail.tsx | both | EventDetailProps | Y | 102 |
|
||||
| EventList | EventList.tsx | both | | Y | 261 |
|
||||
| FAQList | FAQList.tsx | both | | Y | 172 |
|
||||
| InquiryDetail | InquiryDetail.tsx | both | InquiryDetailProps | Y | 359 |
|
||||
| InquiryDetailClientV2 | InquiryDetailClientV2.tsx | both | InquiryDetailClientV2Props | Y | 224 |
|
||||
| InquiryForm | InquiryForm.tsx | both | InquiryFormProps | Y | 237 |
|
||||
| InquiryList | InquiryList.tsx | both | | Y | 292 |
|
||||
| NoticeDetail | NoticeDetail.tsx | both | NoticeDetailProps | Y | 102 |
|
||||
| NoticeList | NoticeList.tsx | both | | Y | 227 |
|
||||
|
||||
### document-system (11)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| ApprovalLine | ApprovalLine.tsx | named | ApprovalLineProps | Y | 170 |
|
||||
| ConstructionApprovalTable | ConstructionApprovalTable.tsx | named | ConstructionApprovalTableProps | Y | 116 |
|
||||
| DocumentContent | DocumentContent.tsx | named | DocumentContentProps | Y | 59 |
|
||||
| DocumentHeader | DocumentHeader.tsx | named | DocumentHeaderProps | Y | 248 |
|
||||
| DocumentToolbar | DocumentToolbar.tsx | named | DocumentToolbarProps | Y | 327 |
|
||||
| DocumentViewer | DocumentViewer.tsx | named | | Y | 378 |
|
||||
| InfoTable | InfoTable.tsx | named | InfoTableProps | Y | 95 |
|
||||
| LotApprovalTable | LotApprovalTable.tsx | named | LotApprovalTableProps | Y | 122 |
|
||||
| QualityApprovalTable | QualityApprovalTable.tsx | named | QualityApprovalTableProps | Y | 123 |
|
||||
| SectionHeader | SectionHeader.tsx | named | SectionHeaderProps | Y | 46 |
|
||||
| SignatureSection | SignatureSection.tsx | named | SignatureSectionProps | Y | 107 |
|
||||
|
||||
### hr (24)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| AttendanceInfoDialog | AttendanceInfoDialog.tsx | named | | Y | 301 |
|
||||
| CardDetail | CardDetail.tsx | named | CardDetailProps | Y | 132 |
|
||||
| CardForm | CardForm.tsx | named | CardFormProps | Y | 246 |
|
||||
| CardManagementUnified | CardManagementUnified.tsx | named | CardManagementUnifiedProps | Y | 267 |
|
||||
| CSVUploadDialog | CSVUploadDialog.tsx | named | CSVUploadDialogProps | Y | 252 |
|
||||
| CSVUploadPage | CSVUploadPage.tsx | named | CSVUploadPageProps | Y | 355 |
|
||||
| DepartmentDialog | DepartmentDialog.tsx | named | | Y | 92 |
|
||||
| DepartmentStats | DepartmentStats.tsx | named | | Y | 18 |
|
||||
| DepartmentToolbar | DepartmentToolbar.tsx | named | | Y | 60 |
|
||||
| DepartmentTree | DepartmentTree.tsx | named | | Y | 70 |
|
||||
| DepartmentTreeItem | DepartmentTreeItem.tsx | named | | Y | 118 |
|
||||
| EmployeeDetail | EmployeeDetail.tsx | named | EmployeeDetailProps | Y | 222 |
|
||||
| EmployeeDialog | EmployeeDialog.tsx | named | | Y | 582 |
|
||||
| EmployeeForm | EmployeeForm.tsx | named | EmployeeFormProps | Y | 1052 |
|
||||
| EmployeeToolbar | EmployeeToolbar.tsx | named | EmployeeToolbarProps | Y | 82 |
|
||||
| FieldSettingsDialog | FieldSettingsDialog.tsx | named | FieldSettingsDialogProps | Y | 259 |
|
||||
| ReasonInfoDialog | ReasonInfoDialog.tsx | named | | Y | 140 |
|
||||
| SalaryDetailDialog | SalaryDetailDialog.tsx | named | SalaryDetailDialogProps | Y | 420 |
|
||||
| UserInviteDialog | UserInviteDialog.tsx | named | UserInviteDialogProps | Y | 116 |
|
||||
| VacationAdjustDialog | VacationAdjustDialog.tsx | named | VacationAdjustDialogProps | Y | 225 |
|
||||
| VacationGrantDialog | VacationGrantDialog.tsx | named | VacationGrantDialogProps | Y | 202 |
|
||||
| VacationRegisterDialog | VacationRegisterDialog.tsx | named | VacationRegisterDialogProps | Y | 201 |
|
||||
| VacationRequestDialog | VacationRequestDialog.tsx | named | VacationRequestDialogProps | Y | 208 |
|
||||
| VacationTypeSettingsDialog | VacationTypeSettingsDialog.tsx | named | VacationTypeSettingsDialogProps | Y | 192 |
|
||||
|
||||
### items (65)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| AssemblyPartForm | AssemblyPartForm.tsx | default | AssemblyPartFormProps | | 337 |
|
||||
| AttributeTabContent | AttributeTabContent.tsx | named | AttributeTabContentProps | Y | 453 |
|
||||
| BendingDiagramSection | BendingDiagramSection.tsx | default | BendingDiagramSectionProps | | 477 |
|
||||
| BendingPartForm | BendingPartForm.tsx | default | BendingPartFormProps | | 304 |
|
||||
| BOMManagementSection | BOMManagementSection.tsx | named | BOMManagementSectionProps | Y | 293 |
|
||||
| BOMSection | BOMSection.tsx | default | BOMSectionProps | | 366 |
|
||||
| CheckboxField | CheckboxField.tsx | named | | Y | 47 |
|
||||
| ColumnDialog | ColumnDialog.tsx | named | ColumnDialogProps | Y | 124 |
|
||||
| ColumnManageDialog | ColumnManageDialog.tsx | named | ColumnManageDialogProps | Y | 210 |
|
||||
| ComputedField | ComputedField.tsx | named | | Y | 136 |
|
||||
| ConditionalDisplayUI | ConditionalDisplayUI.tsx | named | ConditionalDisplayUIProps | | 349 |
|
||||
| CurrencyField | CurrencyField.tsx | named | | Y | 127 |
|
||||
| DateField | DateField.tsx | named | | Y | 45 |
|
||||
| DraggableField | DraggableField.tsx | named | DraggableFieldProps | | 130 |
|
||||
| DraggableSection | DraggableSection.tsx | named | DraggableSectionProps | | 140 |
|
||||
| DrawingCanvas | DrawingCanvas.tsx | named | DrawingCanvasProps | Y | 404 |
|
||||
| DropdownField | DropdownField.tsx | named | | Y | 141 |
|
||||
| DuplicateCodeDialog | DuplicateCodeDialog.tsx | named | DuplicateCodeDialogProps | Y | 49 |
|
||||
| DynamicBOMSection | DynamicBOMSection.tsx | default | DynamicBOMSectionProps | Y | 515 |
|
||||
| DynamicFieldRenderer | DynamicFieldRenderer.tsx | named | | Y | 86 |
|
||||
| DynamicTableSection | DynamicTableSection.tsx | default | DynamicTableSectionProps | Y | 200 |
|
||||
| ErrorAlertDialog | ErrorAlertDialog.tsx | named | ErrorAlertDialogProps | Y | 51 |
|
||||
| ErrorAlertProvider | ErrorAlertContext.tsx | named | ErrorAlertProviderProps | Y | 94 |
|
||||
| FieldDialog | FieldDialog.tsx | named | FieldDialogProps | Y | 478 |
|
||||
| FieldDrawer | FieldDrawer.tsx | named | FieldDrawerProps | Y | 682 |
|
||||
| FileField | FileField.tsx | named | | Y | 200 |
|
||||
| FileUpload | FileUpload.tsx | default | FileUploadProps | Y | 233 |
|
||||
| FileUploadFields | FileUploadFields.tsx | named | FileUploadFieldsProps | Y | 240 |
|
||||
| FormHeader | FormHeader.tsx | named | FormHeaderProps | Y | 31 |
|
||||
| FormHeader | FormHeader.tsx | default | FormHeaderProps | | 62 |
|
||||
| ImportFieldDialog | ImportFieldDialog.tsx | named | ImportFieldDialogProps | Y | 279 |
|
||||
| ImportSectionDialog | ImportSectionDialog.tsx | named | ImportSectionDialogProps | Y | 221 |
|
||||
| ItemDetailClient | ItemDetailClient.tsx | default | ItemDetailClientProps | Y | 638 |
|
||||
| ItemDetailEdit | ItemDetailEdit.tsx | named | ItemDetailEditProps | Y | 390 |
|
||||
| ItemDetailView | ItemDetailView.tsx | named | ItemDetailViewProps | Y | 275 |
|
||||
| ItemFormContext | ItemFormContext.tsx | both | ItemFormProviderProps | Y | 77 |
|
||||
| ItemListClient | ItemListClient.tsx | default | | Y | 607 |
|
||||
| ItemMasterDataManagement | ItemMasterDataManagement.tsx | named | | Y | 1006 |
|
||||
| ItemMasterDialogs | ItemMasterDialogs.tsx | named | ItemMasterDialogsProps | Y | 968 |
|
||||
| ItemTypeSelect | ItemTypeSelect.tsx | default | ItemTypeSelectProps | Y | 76 |
|
||||
| LoadTemplateDialog | LoadTemplateDialog.tsx | named | LoadTemplateDialogProps | Y | 103 |
|
||||
| MasterFieldDialog | MasterFieldDialog.tsx | named | MasterFieldDialogProps | Y | 306 |
|
||||
| MaterialForm | MaterialForm.tsx | default | MaterialFormProps | | 354 |
|
||||
| MultiSelectField | MultiSelectField.tsx | named | | Y | 192 |
|
||||
| NumberField | NumberField.tsx | named | | Y | 58 |
|
||||
| OptionDialog | OptionDialog.tsx | named | OptionDialogProps | Y | 262 |
|
||||
| PageDialog | PageDialog.tsx | named | PageDialogProps | Y | 107 |
|
||||
| PartForm | PartForm.tsx | default | PartFormProps | | 273 |
|
||||
| PathEditDialog | PathEditDialog.tsx | named | PathEditDialogProps | Y | 86 |
|
||||
| ProductForm | ProductForm.tsx | both | ProductFormProps | | 307 |
|
||||
| PurchasedPartForm | PurchasedPartForm.tsx | default | PurchasedPartFormProps | | 336 |
|
||||
| RadioField | RadioField.tsx | named | | Y | 92 |
|
||||
| ReferenceField | ReferenceField.tsx | named | | Y | 168 |
|
||||
| SectionDialog | SectionDialog.tsx | named | SectionDialogProps | Y | 335 |
|
||||
| SectionsTab | SectionsTab.tsx | named | SectionsTabProps | Y | 363 |
|
||||
| SectionTemplateDialog | SectionTemplateDialog.tsx | named | SectionTemplateDialogProps | Y | 180 |
|
||||
| TableCellRenderer | TableCellRenderer.tsx | named | TableCellRendererProps | Y | 85 |
|
||||
| TabManagementDialogs | TabManagementDialogs.tsx | named | TabManagementDialogsProps | Y | 409 |
|
||||
| TemplateFieldDialog | TemplateFieldDialog.tsx | named | TemplateFieldDialogProps | Y | 392 |
|
||||
| TextareaField | TextareaField.tsx | named | | Y | 51 |
|
||||
| TextField | TextField.tsx | named | | Y | 48 |
|
||||
| ToggleField | ToggleField.tsx | named | | Y | 62 |
|
||||
| UnitValueField | UnitValueField.tsx | named | | Y | 129 |
|
||||
| ValidationAlert | ValidationAlert.tsx | named | ValidationAlertProps | Y | 42 |
|
||||
| ValidationAlert | ValidationAlert.tsx | default | ValidationAlertProps | | 50 |
|
||||
|
||||
### material (13)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| ImportInspectionInputModal | ImportInspectionInputModal.tsx | named | ImportInspectionInputModalProps | Y | 798 |
|
||||
| InspectionCreate | InspectionCreate.tsx | named | Props | Y | 364 |
|
||||
| InventoryAdjustmentDialog | InventoryAdjustmentDialog.tsx | named | Props | Y | 236 |
|
||||
| ReceivingDetail | ReceivingDetail.tsx | named | Props | Y | 921 |
|
||||
| ReceivingList | ReceivingList.tsx | named | | Y | 467 |
|
||||
| ReceivingProcessDialog | ReceivingProcessDialog.tsx | named | Props | Y | 238 |
|
||||
| ReceivingReceiptContent | ReceivingReceiptContent.tsx | named | ReceivingReceiptContentProps | Y | 132 |
|
||||
| ReceivingReceiptDialog | ReceivingReceiptDialog.tsx | named | Props | Y | 46 |
|
||||
| StockAuditModal | StockAuditModal.tsx | named | StockAuditModalProps | Y | 237 |
|
||||
| StockStatusDetail | StockStatusDetail.tsx | named | StockStatusDetailProps | Y | 313 |
|
||||
| StockStatusList | StockStatusList.tsx | named | | Y | 473 |
|
||||
| SuccessDialog | SuccessDialog.tsx | named | Props | Y | 49 |
|
||||
| SupplierSearchModal | SupplierSearchModal.tsx | named | SupplierSearchModalProps | Y | 161 |
|
||||
|
||||
### orders (10)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| ContractDocument | ContractDocument.tsx | named | ContractDocumentProps | Y | 246 |
|
||||
| ItemAddDialog | ItemAddDialog.tsx | named | ItemAddDialogProps | Y | 317 |
|
||||
| OrderDocumentModal | OrderDocumentModal.tsx | named | OrderDocumentModalProps | Y | 207 |
|
||||
| OrderRegistration | OrderRegistration.tsx | named | OrderRegistrationProps | Y | 1087 |
|
||||
| OrderSalesDetailEdit | OrderSalesDetailEdit.tsx | named | OrderSalesDetailEditProps | Y | 735 |
|
||||
| OrderSalesDetailView | OrderSalesDetailView.tsx | named | OrderSalesDetailViewProps | Y | 824 |
|
||||
| PurchaseOrderDocument | PurchaseOrderDocument.tsx | named | PurchaseOrderDocumentProps | Y | 223 |
|
||||
| QuotationSelectDialog | QuotationSelectDialog.tsx | named | QuotationSelectDialogProps | Y | 114 |
|
||||
| SalesOrderDocument | SalesOrderDocument.tsx | named | SalesOrderDocumentProps | Y | 638 |
|
||||
| TransactionDocument | TransactionDocument.tsx | named | TransactionDocumentProps | Y | 226 |
|
||||
|
||||
### outbound (11)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| DeliveryConfirmation | DeliveryConfirmation.tsx | named | DeliveryConfirmationProps | Y | 18 |
|
||||
| ShipmentCreate | ShipmentCreate.tsx | named | | Y | 772 |
|
||||
| ShipmentDetail | ShipmentDetail.tsx | named | ShipmentDetailProps | Y | 671 |
|
||||
| ShipmentEdit | ShipmentEdit.tsx | named | ShipmentEditProps | Y | 791 |
|
||||
| ShipmentList | ShipmentList.tsx | named | | Y | 399 |
|
||||
| ShipmentOrderDocument | ShipmentOrderDocument.tsx | named | ShipmentOrderDocumentProps | Y | 647 |
|
||||
| ShippingSlip | ShippingSlip.tsx | named | ShippingSlipProps | Y | 18 |
|
||||
| TransactionStatement | TransactionStatement.tsx | named | TransactionStatementProps | Y | 154 |
|
||||
| VehicleDispatchDetail | VehicleDispatchDetail.tsx | named | VehicleDispatchDetailProps | Y | 181 |
|
||||
| VehicleDispatchEdit | VehicleDispatchEdit.tsx | named | VehicleDispatchEditProps | Y | 399 |
|
||||
| VehicleDispatchList | VehicleDispatchList.tsx | named | | Y | 331 |
|
||||
|
||||
### pricing (5)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| PricingFinalizeDialog | PricingFinalizeDialog.tsx | both | PricingFinalizeDialogProps | Y | 95 |
|
||||
| PricingFormClient | PricingFormClient.tsx | both | PricingFormClientProps | Y | 780 |
|
||||
| PricingHistoryDialog | PricingHistoryDialog.tsx | both | PricingHistoryDialogProps | Y | 170 |
|
||||
| PricingListClient | PricingListClient.tsx | both | PricingListClientProps | Y | 387 |
|
||||
| PricingRevisionDialog | PricingRevisionDialog.tsx | both | PricingRevisionDialogProps | Y | 95 |
|
||||
|
||||
### pricing-distribution (3)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| PriceDistributionDetail | PriceDistributionDetail.tsx | both | Props | Y | 539 |
|
||||
| PriceDistributionDocumentModal | PriceDistributionDocumentModal.tsx | both | Props | Y | 158 |
|
||||
| PriceDistributionList | PriceDistributionList.tsx | both | | Y | 328 |
|
||||
|
||||
### pricing-table-management (3)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| PricingTableDetailClient | PricingTableDetailClient.tsx | named | PricingTableDetailClientProps | Y | 93 |
|
||||
| PricingTableForm | PricingTableForm.tsx | named | PricingTableFormProps | Y | 486 |
|
||||
| PricingTableListClient | PricingTableListClient.tsx | default | | Y | 381 |
|
||||
|
||||
### process-management (12)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| InspectionPreviewModal | InspectionPreviewModal.tsx | named | InspectionPreviewModalProps | Y | 265 |
|
||||
| InspectionSettingModal | InspectionSettingModal.tsx | named | InspectionSettingModalProps | Y | 294 |
|
||||
| ProcessDetail | ProcessDetail.tsx | named | ProcessDetailProps | Y | 451 |
|
||||
| ProcessDetailClientV2 | ProcessDetailClientV2.tsx | named | ProcessDetailClientV2Props | Y | 137 |
|
||||
| ProcessForm | ProcessForm.tsx | named | ProcessFormProps | Y | 829 |
|
||||
| ProcessListClient | ProcessListClient.tsx | default | ProcessListClientProps | Y | 546 |
|
||||
| ProcessWorkLogContent | ProcessWorkLogContent.tsx | named | ProcessWorkLogContentProps | Y | 136 |
|
||||
| ProcessWorkLogPreviewModal | ProcessWorkLogPreviewModal.tsx | named | ProcessWorkLogPreviewModalProps | Y | 45 |
|
||||
| RuleModal | RuleModal.tsx | named | RuleModalProps | Y | 352 |
|
||||
| StepDetail | StepDetail.tsx | named | StepDetailProps | Y | 212 |
|
||||
| StepDetailClient | StepDetailClient.tsx | named | StepDetailClientProps | Y | 115 |
|
||||
| StepForm | StepForm.tsx | named | StepFormProps | Y | 397 |
|
||||
|
||||
### production (31)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| AssigneeSelectModal | AssigneeSelectModal.tsx | named | AssigneeSelectModalProps | Y | 317 |
|
||||
| BendingInspectionContent | BendingInspectionContent.tsx | named | BendingInspectionContentProps | Y | 490 |
|
||||
| BendingWipInspectionContent | BendingWipInspectionContent.tsx | named | BendingWipInspectionContentProps | Y | 304 |
|
||||
| BendingWorkLogContent | BendingWorkLogContent.tsx | named | BendingWorkLogContentProps | Y | 194 |
|
||||
| CompletionConfirmDialog | CompletionConfirmDialog.tsx | named | CompletionConfirmDialogProps | Y | 64 |
|
||||
| CompletionToast | CompletionToast.tsx | named | CompletionToastProps | Y | 28 |
|
||||
| InspectionCheckbox | inspection-shared.tsx | named | | Y | 282 |
|
||||
| InspectionInputModal | InspectionInputModal.tsx | named | InspectionInputModalProps | Y | 978 |
|
||||
| InspectionReportModal | InspectionReportModal.tsx | named | InspectionReportModalProps | Y | 409 |
|
||||
| IssueReportModal | IssueReportModal.tsx | named | IssueReportModalProps | Y | 178 |
|
||||
| MaterialInputModal | MaterialInputModal.tsx | named | MaterialInputModalProps | Y | 333 |
|
||||
| ProcessDetailSection | ProcessDetailSection.tsx | named | ProcessDetailSectionProps | Y | 392 |
|
||||
| SalesOrderSelectModal | SalesOrderSelectModal.tsx | named | SalesOrderSelectModalProps | Y | 102 |
|
||||
| ScreenInspectionContent | ScreenInspectionContent.tsx | named | ScreenInspectionContentProps | Y | 310 |
|
||||
| ScreenWorkLogContent | ScreenWorkLogContent.tsx | named | ScreenWorkLogContentProps | Y | 201 |
|
||||
| SlatInspectionContent | SlatInspectionContent.tsx | named | SlatInspectionContentProps | Y | 297 |
|
||||
| SlatJointBarInspectionContent | SlatJointBarInspectionContent.tsx | named | SlatJointBarInspectionContentProps | Y | 311 |
|
||||
| SlatWorkLogContent | SlatWorkLogContent.tsx | named | SlatWorkLogContentProps | Y | 198 |
|
||||
| TemplateInspectionContent | TemplateInspectionContent.tsx | named | TemplateInspectionContentProps | Y | 719 |
|
||||
| WipProductionModal | WipProductionModal.tsx | named | WipProductionModalProps | Y | 272 |
|
||||
| WorkCard | WorkCard.tsx | named | WorkCardProps | Y | 188 |
|
||||
| WorkCompletionResultDialog | WorkCompletionResultDialog.tsx | named | WorkCompletionResultDialogProps | Y | 85 |
|
||||
| WorkItemCard | WorkItemCard.tsx | named | WorkItemCardProps | Y | 382 |
|
||||
| WorkLogContent | WorkLogContent.tsx | named | WorkLogContentProps | Y | 195 |
|
||||
| WorkLogModal | WorkLogModal.tsx | named | WorkLogModalProps | Y | 152 |
|
||||
| WorkOrderCreate | WorkOrderCreate.tsx | named | | Y | 545 |
|
||||
| WorkOrderDetail | WorkOrderDetail.tsx | named | WorkOrderDetailProps | Y | 656 |
|
||||
| WorkOrderEdit | WorkOrderEdit.tsx | named | WorkOrderEditProps | Y | 656 |
|
||||
| WorkOrderList | WorkOrderList.tsx | named | | Y | 460 |
|
||||
| WorkOrderListPanel | WorkOrderListPanel.tsx | named | WorkOrderListPanelProps | Y | 132 |
|
||||
| WorkResultList | WorkResultList.tsx | named | | Y | 374 |
|
||||
|
||||
### quality (11)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| InspectionCreate | InspectionCreate.tsx | named | | Y | 695 |
|
||||
| InspectionDetail | InspectionDetail.tsx | named | InspectionDetailProps | Y | 1126 |
|
||||
| InspectionList | InspectionList.tsx | named | | Y | 388 |
|
||||
| InspectionReportDocument | InspectionReportDocument.tsx | named | InspectionReportDocumentProps | Y | 416 |
|
||||
| InspectionReportModal | InspectionReportModal.tsx | named | InspectionReportModalProps | Y | 170 |
|
||||
| InspectionRequestDocument | InspectionRequestDocument.tsx | named | InspectionRequestDocumentProps | Y | 258 |
|
||||
| InspectionRequestModal | InspectionRequestModal.tsx | named | InspectionRequestModalProps | Y | 40 |
|
||||
| MemoModal | MemoModal.tsx | named | MemoModalProps | Y | 92 |
|
||||
| OrderSelectModal | OrderSelectModal.tsx | named | OrderSelectModalProps | Y | 111 |
|
||||
| PerformanceReportList | PerformanceReportList.tsx | named | | Y | 604 |
|
||||
| ProductInspectionInputModal | ProductInspectionInputModal.tsx | named | ProductInspectionInputModalProps | Y | 486 |
|
||||
|
||||
### quotes (15)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| DiscountModal | DiscountModal.tsx | named | DiscountModalProps | Y | 232 |
|
||||
| FormulaViewModal | FormulaViewModal.tsx | named | FormulaViewModalProps | Y | 316 |
|
||||
| ItemSearchModal | ItemSearchModal.tsx | named | ItemSearchModalProps | Y | 114 |
|
||||
| LocationDetailPanel | LocationDetailPanel.tsx | named | LocationDetailPanelProps | Y | 827 |
|
||||
| LocationEditModal | LocationEditModal.tsx | named | LocationEditModalProps | Y | 283 |
|
||||
| LocationListPanel | LocationListPanel.tsx | named | LocationListPanelProps | Y | 575 |
|
||||
| PurchaseOrderDocument | PurchaseOrderDocument.tsx | named | PurchaseOrderDocumentProps | | 265 |
|
||||
| QuoteDocument | QuoteDocument.tsx | named | QuoteDocumentProps | | 409 |
|
||||
| QuoteFooterBar | QuoteFooterBar.tsx | named | QuoteFooterBarProps | Y | 236 |
|
||||
| QuoteManagementClient | QuoteManagementClient.tsx | named | QuoteManagementClientProps | Y | 713 |
|
||||
| QuotePreviewContent | QuotePreviewContent.tsx | named | QuotePreviewContentProps | Y | 434 |
|
||||
| QuotePreviewModal | QuotePreviewModal.tsx | named | QuotePreviewModalProps | Y | 132 |
|
||||
| QuoteRegistration | QuoteRegistration.tsx | named | QuoteRegistrationProps | Y | 1023 |
|
||||
| QuoteSummaryPanel | QuoteSummaryPanel.tsx | named | QuoteSummaryPanelProps | Y | 277 |
|
||||
| QuoteTransactionModal | QuoteTransactionModal.tsx | named | QuoteTransactionModalProps | Y | 324 |
|
||||
|
||||
### settings (16)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| AccountDetail | AccountDetail.tsx | named | AccountDetailProps | Y | 356 |
|
||||
| AccountDetail | AccountDetail.tsx | named | AccountDetailProps | Y | 369 |
|
||||
| AddCompanyDialog | AddCompanyDialog.tsx | named | AddCompanyDialogProps | Y | 149 |
|
||||
| ItemSettingsDialog | ItemSettingsDialog.tsx | named | ItemSettingsDialogProps | Y | 336 |
|
||||
| PaymentHistoryClient | PaymentHistoryClient.tsx | named | PaymentHistoryClientProps | Y | 255 |
|
||||
| PermissionDetail | PermissionDetail.tsx | named | PermissionDetailProps | Y | 456 |
|
||||
| PermissionDetailClient | PermissionDetailClient.tsx | named | PermissionDetailClientProps | Y | 700 |
|
||||
| PermissionDialog | PermissionDialog.tsx | named | | Y | 109 |
|
||||
| PopupDetail | PopupDetail.tsx | both | PopupDetailProps | Y | 125 |
|
||||
| PopupDetailClientV2 | PopupDetailClientV2.tsx | named | PopupDetailClientV2Props | Y | 199 |
|
||||
| PopupForm | PopupForm.tsx | both | PopupFormProps | Y | 319 |
|
||||
| PopupList | PopupList.tsx | both | PopupListProps | Y | 198 |
|
||||
| RankDialog | RankDialog.tsx | named | | Y | 89 |
|
||||
| SubscriptionClient | SubscriptionClient.tsx | named | SubscriptionClientProps | Y | 242 |
|
||||
| SubscriptionManagement | SubscriptionManagement.tsx | named | SubscriptionManagementProps | Y | 250 |
|
||||
| TitleDialog | TitleDialog.tsx | named | | Y | 90 |
|
||||
|
||||
### templates (11)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| DetailActions | DetailActions.tsx | both | DetailActionsProps | Y | 172 |
|
||||
| DetailField | DetailField.tsx | both | DetailFieldProps | Y | 91 |
|
||||
| DetailFieldSkeleton | DetailFieldSkeleton.tsx | both | DetailFieldSkeletonProps | Y | 48 |
|
||||
| DetailGrid | DetailGrid.tsx | both | DetailGridProps | Y | 63 |
|
||||
| DetailGridSkeleton | DetailGridSkeleton.tsx | both | DetailGridSkeletonProps | Y | 61 |
|
||||
| DetailSection | DetailSection.tsx | both | DetailSectionProps | Y | 97 |
|
||||
| DetailSectionSkeleton | DetailSectionSkeleton.tsx | both | DetailSectionSkeletonProps | Y | 53 |
|
||||
| DetailSectionSkeleton | skeletons.tsx | both | DetailFieldSkeletonProps | Y | 183 |
|
||||
| FieldInput | FieldInput.tsx | both | FieldInputProps | Y | 408 |
|
||||
| FieldRenderer | FieldRenderer.tsx | named | FieldRendererProps | Y | 390 |
|
||||
| IntegratedListTemplateV2 | IntegratedListTemplateV2.tsx | named | IntegratedListTemplateV2Props | Y | 1087 |
|
||||
|
||||
### vehicle-management (3)
|
||||
|
||||
| Component | File | Export | Props | Client | Lines |
|
||||
|-----------|------|--------|-------|--------|-------|
|
||||
| Config | config.tsx | none | | Y | 431 |
|
||||
| Config | config.tsx | none | | Y | 479 |
|
||||
| Config | config.tsx | none | | Y | 266 |
|
||||
@@ -0,0 +1,98 @@
|
||||
# [IMPL-2026-01-05] 카테고리관리 페이지 구현 체크리스트
|
||||
|
||||
## 개요
|
||||
- **위치**: 발주관리 > 기준정보 > 카테고리관리
|
||||
- **URL**: `/ko/juil/order/base-info/categories`
|
||||
- **참조 페이지**: `/ko/settings/ranks` (직급관리)
|
||||
- **기능**: 동일, 텍스트/라벨만 다름
|
||||
|
||||
## 스크린샷 분석
|
||||
|
||||
### UI 구성
|
||||
| 구성요소 | 내용 |
|
||||
|---------|------|
|
||||
| 타이틀 | 카테고리관리 |
|
||||
| 설명 | 카테고리를 등록하고 관리합니다. |
|
||||
| 입력필드 라벨 | 카테고리 |
|
||||
| 입력필드 placeholder | 카테고리를 입력해주세요 |
|
||||
| 테이블 컬럼 | 카테고리, 작업 |
|
||||
| 기본 데이터 | 슬라이드 OPEN 사이즈, 모터, 공정자재, 철물 |
|
||||
|
||||
### Description 영역 (참고용, UI 미구현)
|
||||
1. 추가 버튼 클릭 시 목록 최하단에 추가
|
||||
2. 드래그&드롭으로 순서 변경
|
||||
3. 수정 버튼 → 수정 팝업
|
||||
4. 삭제 버튼 → 조건별 Alert:
|
||||
- 품목 사용 중: "(카테고리명)을 사용하고 있는 품목이 있습니다. 모두 변경 후 삭제가 가능합니다."
|
||||
- 미사용: "정말 삭제하시겠습니까?" → "삭제가 되었습니다."
|
||||
- 기본 카테고리: "기본 카테고리는 삭제가 불가합니다."
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
### Phase 1: 파일 구조 생성
|
||||
- [x] `src/app/[locale]/(protected)/juil/order/base-info/categories/page.tsx` 생성
|
||||
- [x] `src/components/business/juil/category-management/` 디렉토리 생성
|
||||
|
||||
### Phase 2: 컴포넌트 구현 (RankManagement 복제 + 수정)
|
||||
- [x] `index.tsx` - CategoryManagement 메인 컴포넌트
|
||||
- 타이틀: "카테고리관리"
|
||||
- 설명: "카테고리를 등록하고 관리합니다. 드래그하여 순서를 변경할 수 있습니다."
|
||||
- 아이콘: `FolderTree`
|
||||
- 입력 placeholder: "카테고리를 입력해주세요"
|
||||
- [x] `types.ts` - Category 타입 정의
|
||||
- [x] `actions.ts` - Server Actions (목데이터)
|
||||
- [x] `CategoryDialog.tsx` - 수정 다이얼로그
|
||||
|
||||
### Phase 3: 텍스트 변경 사항
|
||||
| 원본 (ranks) | 변경 (categories) | 상태 |
|
||||
|-------------|-------------------|------|
|
||||
| 직급 | 카테고리 | ✅ |
|
||||
| 직급관리 | 카테고리관리 | ✅ |
|
||||
| 사원의 직급을 관리합니다 | 카테고리를 등록하고 관리합니다 | ✅ |
|
||||
| 직급명을 입력하세요 | 카테고리를 입력해주세요 | ✅ |
|
||||
| 직급이 추가되었습니다 | 카테고리가 추가되었습니다 | ✅ |
|
||||
| 직급이 수정되었습니다 | 카테고리가 수정되었습니다 | ✅ |
|
||||
| 직급이 삭제되었습니다 | 카테고리가 삭제되었습니다 | ✅ |
|
||||
| 등록된 직급이 없습니다 | 등록된 카테고리가 없습니다 | ✅ |
|
||||
|
||||
### Phase 4: 삭제 로직 (삭제 조건 처리)
|
||||
- [x] 기본 카테고리 삭제 불가 로직 추가 (`isDefault` 플래그)
|
||||
- [x] 조건별 Alert 메시지 분기 (actions.ts의 `errorType` 반환)
|
||||
- [ ] 품목 사용 여부 체크 로직 추가 (추후 API 연동 시)
|
||||
|
||||
### Phase 5: 목데이터 설정
|
||||
- [x] 기본 카테고리 4개 설정 완료
|
||||
```typescript
|
||||
const mockCategories = [
|
||||
{ id: '1', name: '슬라이드 OPEN 사이즈', order: 1, isDefault: true },
|
||||
{ id: '2', name: '모터', order: 2, isDefault: true },
|
||||
{ id: '3', name: '공정자재', order: 3, isDefault: true },
|
||||
{ id: '4', name: '철물', order: 4, isDefault: true },
|
||||
];
|
||||
```
|
||||
|
||||
### Phase 6: 테스트 URL 문서 업데이트
|
||||
- [x] `claudedocs/[REF] juil-pages-test-urls.md` 업데이트
|
||||
- 발주관리 > 기준정보 섹션 추가
|
||||
- 카테고리관리 URL 추가
|
||||
|
||||
## 파일 구조
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/[locale]/(protected)/juil/order/
|
||||
│ └── base-info/
|
||||
│ └── categories/
|
||||
│ └── page.tsx
|
||||
└── components/business/juil/
|
||||
└── category-management/
|
||||
├── index.tsx
|
||||
├── types.ts
|
||||
├── actions.ts
|
||||
└── CategoryDialog.tsx
|
||||
```
|
||||
|
||||
## 진행 상태
|
||||
- 생성일: 2026-01-05
|
||||
- 상태: ✅ 완료 (목데이터 기반)
|
||||
- 남은 작업: API 연동 시 품목 사용 여부 체크 로직 추가
|
||||
@@ -0,0 +1,209 @@
|
||||
# [IMPL-2026-01-05] 품목관리 페이지 구현 체크리스트
|
||||
|
||||
## 개요
|
||||
- **위치**: 발주관리 > 기준정보 > 품목관리
|
||||
- **URL**: `/ko/juil/order/base-info/items`
|
||||
- **참조 템플릿**: IntegratedListTemplateV2 (리스트 페이지 표준)
|
||||
- **기능**: 품목 CRUD, 필터링, 검색, 정렬
|
||||
|
||||
## 스크린샷 분석
|
||||
|
||||
### 헤더 영역
|
||||
| 구성요소 | 내용 |
|
||||
|---------|------|
|
||||
| 타이틀 | 품목관리 |
|
||||
| 설명 | 품목을 등록하여 관리합니다. |
|
||||
| 날짜 필터 | 날짜 범위 선택 (DateRangePicker) |
|
||||
| 빠른 날짜 버튼 | 전체년도, 전전월, 전월, 당월, 어제, 오늘 |
|
||||
| 액션 버튼 | 품목 등록 (빨간색 primary) |
|
||||
|
||||
### 통계 카드
|
||||
| 카드 | 내용 |
|
||||
|------|------|
|
||||
| 전체 품목 | 전체 품목 수 표시 |
|
||||
| 사용 품목 | 사용 중인 품목 수 표시 |
|
||||
|
||||
### 검색 및 필터 영역
|
||||
| 구성요소 | 내용 |
|
||||
|---------|------|
|
||||
| 검색 입력 | 품목명 검색 |
|
||||
| 선택 카운트 | N건 / N건 선택 |
|
||||
| 삭제 버튼 | 선택된 항목 일괄 삭제 |
|
||||
|
||||
### 테이블 컬럼
|
||||
| 컬럼 | 타입 | 필터 옵션 |
|
||||
|------|------|----------|
|
||||
| 체크박스 | checkbox | - |
|
||||
| 품목번호 | text | - |
|
||||
| 물품유형 | select filter | 전체, 제품, 부품, 소모품, 공과 |
|
||||
| 카테고리 | select filter + search | 전체, 기본, (카테고리 목록) |
|
||||
| 품목명 | text | - |
|
||||
| 규격 | select filter | 전체, 인정, 비인정 |
|
||||
| 단위 | text | - |
|
||||
| 구분 | select filter | 전체, 경품발주, 원자재발주, 외주발주 |
|
||||
| 상태 | badge | 승인, 작업 |
|
||||
| 작업 | actions | 수정(연필 아이콘) |
|
||||
|
||||
### Description 영역 (참고용, UI 미구현)
|
||||
1. 품목 등록 버튼 - 클릭 시 품목 상세 등록 화면으로 이동
|
||||
2. 물품유형 셀렉트 박스 - 전체/제품/부품/소모품/공과 (디폴트: 전체)
|
||||
3. 카테고리 셀렉트 박스, 검색 - 전체/기본/카테고리 목록 (디폴트: 전체)
|
||||
4. 규격 셀렉트 박스 - 전체/인정/비인정 (디폴트: 전체)
|
||||
5. 구분 셀렉트 박스 - 전체/경품발주/원자재발주/외주발주 (디폴트: 전체)
|
||||
6. 상태 셀렉트 박스 - 전체/사용/중지 (디폴트: 전체)
|
||||
7. 정렬 셀렉트 박스 - 최신순/등록순 (디폴트: 최신순)
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
### Phase 1: 파일 구조 생성
|
||||
- [x] `src/app/[locale]/(protected)/juil/order/base-info/items/page.tsx` 생성
|
||||
- [x] `src/components/business/juil/item-management/` 디렉토리 생성
|
||||
|
||||
### Phase 2: 타입 및 상수 정의
|
||||
- [x] `types.ts` - Item 타입 정의
|
||||
```typescript
|
||||
interface Item {
|
||||
id: string;
|
||||
itemNumber: string; // 품목번호
|
||||
itemType: ItemType; // 물품유형
|
||||
categoryId: string; // 카테고리 ID
|
||||
categoryName: string; // 카테고리명
|
||||
itemName: string; // 품목명
|
||||
specification: string; // 규격 (인쇄/비인쇄)
|
||||
unit: string; // 단위
|
||||
orderType: OrderType; // 구분
|
||||
status: ItemStatus; // 상태
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
```
|
||||
- [x] `constants.ts` - 필터 옵션 상수 정의
|
||||
```typescript
|
||||
// 물품유형
|
||||
const ITEM_TYPES = ['전체', '제품', '부품', '소모품', '공과'];
|
||||
|
||||
// 규격
|
||||
const SPECIFICATIONS = ['전체', '인정', '비인정'];
|
||||
|
||||
// 구분
|
||||
const ORDER_TYPES = ['전체', '경품발주', '원자재발주', '외주발주'];
|
||||
|
||||
// 상태
|
||||
const ITEM_STATUSES = ['전체', '사용', '중지'];
|
||||
|
||||
// 정렬
|
||||
const SORT_OPTIONS = ['최신순', '등록순'];
|
||||
```
|
||||
|
||||
### Phase 3: 메인 컴포넌트 구현
|
||||
- [x] `index.tsx` - ItemManagement 메인 컴포넌트 (export)
|
||||
- [x] `ItemManagementClient.tsx` - 클라이언트 컴포넌트
|
||||
- IntegratedListTemplateV2 사용
|
||||
- 헤더: 타이틀, 설명, 날짜필터, 품목등록 버튼
|
||||
- 통계 카드: StatCards 컴포넌트 활용
|
||||
- 테이블: 컬럼 헤더 필터 포함
|
||||
- 검색 및 삭제 기능
|
||||
|
||||
### Phase 4: 테이블 컬럼 설정
|
||||
- [x] 테이블 컬럼 정의 (ItemManagementClient.tsx 내 포함)
|
||||
- 체크박스 컬럼
|
||||
- 품목번호 컬럼
|
||||
- 물품유형 컬럼 (헤더 필터 Select)
|
||||
- 카테고리 컬럼 (헤더 필터 Select + 검색)
|
||||
- 품목명 컬럼
|
||||
- 규격 컬럼 (헤더 필터 Select)
|
||||
- 단위 컬럼
|
||||
- 구분 컬럼 (헤더 필터 Select)
|
||||
- 상태 컬럼 (Badge 표시)
|
||||
- 작업 컬럼 (수정 버튼)
|
||||
|
||||
### Phase 5: Server Actions (목데이터)
|
||||
- [x] `actions.ts` - Server Actions 구현
|
||||
- `getItemList()` - 품목 목록 조회
|
||||
- `getItemStats()` - 통계 조회
|
||||
- `deleteItem()` - 품목 삭제
|
||||
- `deleteItems()` - 품목 일괄 삭제
|
||||
- `getCategoryOptions()` - 카테고리 목록 조회
|
||||
|
||||
### Phase 6: 목데이터 설정
|
||||
```typescript
|
||||
const mockItems: Item[] = [
|
||||
{ id: '1', itemNumber: '123123', itemType: '제품', categoryName: '카테고리명', itemName: '품목명', specification: '인쇄', unit: 'SET', orderType: '외주발주', status: '승인' },
|
||||
{ id: '2', itemNumber: '123123', itemType: '부품', categoryName: '카테고리명', itemName: '품목명', specification: '비인쇄', unit: 'SET', orderType: '외주발주', status: '승인' },
|
||||
{ id: '3', itemNumber: '123123', itemType: '소모품', categoryName: '카테고리명', itemName: '품목명', specification: '인쇄', unit: 'SET', orderType: '외주발주', status: '승인' },
|
||||
{ id: '4', itemNumber: '123123', itemType: '공과', categoryName: '카테고리명', itemName: '품목명', specification: '비인쇄', unit: 'EA', orderType: '공과', status: '작업' },
|
||||
{ id: '5', itemNumber: '123123', itemType: '부품', categoryName: '카테고리명', itemName: '품목명', specification: '인쇄', unit: 'EA', orderType: '원자재발주', status: '작업' },
|
||||
{ id: '6', itemNumber: '123123', itemType: '소모품', categoryName: '카테고리명', itemName: '품목명', specification: '비인쇄', unit: '승인', orderType: '외주발주', status: '작업' },
|
||||
{ id: '7', itemNumber: '123123', itemType: '소모품', categoryName: '카테고리명', itemName: '품목명', specification: '인쇄', unit: '승인', orderType: '공과', status: '작업' },
|
||||
];
|
||||
|
||||
const mockStats = {
|
||||
totalItems: 7,
|
||||
activeItems: 5,
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 7: 헤더 필터 컴포넌트
|
||||
- [x] tableHeaderActions 영역에 Select 필터 구현
|
||||
- 물품유형 필터
|
||||
- 규격 필터
|
||||
- 구분 필터
|
||||
- 정렬 필터
|
||||
|
||||
### Phase 8: 등록/상세/수정 페이지 구현
|
||||
- [x] 품목 등록 버튼 클릭 → `/ko/juil/order/base-info/items/new` 이동
|
||||
- [x] 수정 버튼 클릭 → `/ko/juil/order/base-info/items/[id]?mode=edit` 이동
|
||||
- [x] 등록/수정/상세 페이지 구현 (ItemDetailClient.tsx)
|
||||
- [x] Server Actions (getItem, createItem, updateItem) 구현
|
||||
- [x] 발주 항목 동적 추가/삭제 기능
|
||||
|
||||
### Phase 9: 테스트 URL 문서 업데이트
|
||||
- [x] `claudedocs/[REF] juil-pages-test-urls.md` 업데이트
|
||||
- 품목관리 URL 추가
|
||||
|
||||
## 파일 구조
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/[locale]/(protected)/juil/order/
|
||||
│ └── base-info/
|
||||
│ └── items/
|
||||
│ ├── page.tsx
|
||||
│ ├── new/
|
||||
│ │ └── page.tsx
|
||||
│ └── [id]/
|
||||
│ └── page.tsx
|
||||
└── components/business/juil/
|
||||
└── item-management/
|
||||
├── index.tsx
|
||||
├── ItemManagementClient.tsx
|
||||
├── ItemDetailClient.tsx
|
||||
├── types.ts
|
||||
├── constants.ts
|
||||
└── actions.ts
|
||||
```
|
||||
|
||||
## 참조 컴포넌트
|
||||
- `IntegratedListTemplateV2` - 리스트 템플릿
|
||||
- `StatCards` - 통계 카드
|
||||
- `DateRangePicker` - 날짜 범위 선택
|
||||
- `Select` - 필터 셀렉트박스
|
||||
- `Badge` - 상태 표시
|
||||
- `Button` - 버튼
|
||||
- `Checkbox` - 체크박스
|
||||
|
||||
## UI 구현 참고
|
||||
- 컬럼 헤더 내 필터 Select: 기존 프로젝트 내 유사 구현 검색 필요
|
||||
- 날짜 빠른 선택 버튼 그룹: 기존 컴포넌트 활용 또는 신규 구현
|
||||
|
||||
## 진행 상태
|
||||
- 생성일: 2026-01-05
|
||||
- 상태: ✅ 전체 완료 (리스트 + 상세/등록/수정)
|
||||
|
||||
## 히스토리
|
||||
| 날짜 | 작업 내용 | 상태 |
|
||||
|------|----------|------|
|
||||
| 2026-01-05 | 체크리스트 작성 | ✅ |
|
||||
| 2026-01-05 | 리스트 페이지 구현 (Phase 1-7, 9) | ✅ |
|
||||
| 2026-01-05 | 규격 필터 수정 (인쇄/비인쇄 → 인정/비인정) | ✅ |
|
||||
| 2026-01-05 | 상세/등록/수정 페이지 구현 (Phase 8) | ✅ |
|
||||
@@ -0,0 +1,119 @@
|
||||
# [IMPL-2026-01-05] 단가관리 리스트 페이지 구현 체크리스트
|
||||
|
||||
## 개요
|
||||
- **위치**: 발주관리 > 기준정보 > 단가관리
|
||||
- **URL**: `/ko/juil/order/base-info/pricing`
|
||||
- **참조 페이지**: `/ko/juil/order/order-management` (OrderManagementListClient)
|
||||
- **패턴**: IntegratedListTemplateV2 + StatCards
|
||||
|
||||
## 스크린샷 분석
|
||||
|
||||
### UI 구성
|
||||
|
||||
#### 1. 헤더 영역
|
||||
| 구성요소 | 내용 |
|
||||
|---------|------|
|
||||
| 타이틀 | 단가관리 |
|
||||
| 설명 | 단가를 등록하고 관리합니다. |
|
||||
|
||||
#### 2. 달력 + 액션 버튼 영역
|
||||
| 구성요소 | 내용 |
|
||||
|---------|------|
|
||||
| 날짜 선택 | DateRangeSelector (2025-09-01 ~ 2025-09-03) |
|
||||
| 액션 버튼들 | 담당단가, 진행단가, 확정, 발행, 이력, 오류, **단가 등록** |
|
||||
|
||||
#### 3. StatCards (통계 카드)
|
||||
| 카드 | 값 | 설명 |
|
||||
|------|-----|------|
|
||||
| 미완료 | 9 | 미완료 단가 |
|
||||
| 확정 | 5 | 확정된 단가 |
|
||||
| 발행 | 4 | 발행된 단가 |
|
||||
|
||||
#### 4. 필터 영역 (테이블 헤더)
|
||||
| 필터 | 옵션 | 기본값 |
|
||||
|------|------|--------|
|
||||
| 품목유형 | 전체, 박스, 부속, 소모품, 공과 | 전체 |
|
||||
| 카테고리 | 전기, (카테고리 목록) | - |
|
||||
| 규격 | 전체, 진행, 미진행 | 전체 |
|
||||
| 구분 | 전체, 금동량, 임의적용가, 미구분 | 전체 |
|
||||
| 상세 | 전체, 사용, 유지, 미등록 | 전체 |
|
||||
| 정렬 | 최신순, 등록순 | 최신순 |
|
||||
|
||||
#### 5. 테이블 컬럼
|
||||
| 컬럼 | 설명 |
|
||||
|------|------|
|
||||
| 체크박스 | 행 선택 |
|
||||
| 단가번호 | 단가 고유번호 |
|
||||
| 품목유형 | 박스/부속/소모품/공과 |
|
||||
| 카테고리 | 품목 카테고리 |
|
||||
| 품목 | 품목명 |
|
||||
| 금액량 | 수량 정보 |
|
||||
| 정량 | 정량 정보 |
|
||||
| 단가 | 단가 금액 |
|
||||
| 구매처 | 구매처 정보 |
|
||||
| 예상단가 | 예상 단가 |
|
||||
| 이전단가 | 이전 단가 |
|
||||
| 판매단가 | 판매 단가 |
|
||||
| 실적 | 실적 정보 |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
### Phase 1: 파일 구조 생성
|
||||
- [x] `src/app/[locale]/(protected)/juil/order/base-info/pricing/page.tsx` 생성
|
||||
- [x] `src/components/business/juil/pricing-management/` 디렉토리 생성
|
||||
|
||||
### Phase 2: 타입 및 상수 정의
|
||||
- [x] `types.ts` - Pricing 타입, 필터 옵션, 상태 스타일
|
||||
- Pricing 인터페이스
|
||||
- PricingStats 인터페이스
|
||||
- 품목유형 옵션 (ITEM_TYPE_OPTIONS)
|
||||
- 규격 옵션 (SPEC_OPTIONS)
|
||||
- 구분 옵션 (DIVISION_OPTIONS)
|
||||
- 상세 옵션 (DETAIL_OPTIONS)
|
||||
- 정렬 옵션 (SORT_OPTIONS)
|
||||
- 상태 스타일 (PRICING_STATUS_STYLES)
|
||||
|
||||
### Phase 3: Server Actions (목데이터)
|
||||
- [x] `actions.ts`
|
||||
- getPricingList() - 목록 조회
|
||||
- getPricingStats() - 통계 조회
|
||||
- deletePricing() - 단일 삭제
|
||||
- deletePricings() - 일괄 삭제
|
||||
|
||||
### Phase 4: 리스트 컴포넌트
|
||||
- [x] `PricingListClient.tsx`
|
||||
- IntegratedListTemplateV2 사용
|
||||
- DateRangeSelector (날짜 범위 선택)
|
||||
- StatCards (미완료/확정/발행)
|
||||
- 필터 셀렉트 박스들 (품목유형, 규격, 구분, 상세, 정렬)
|
||||
- 액션 버튼들 (담당단가, 진행단가, 확정, 발행, 이력, 오류, 단가 등록)
|
||||
- 테이블 렌더링
|
||||
- 모바일 카드 렌더링
|
||||
- 삭제 다이얼로그
|
||||
|
||||
### Phase 5: 목데이터 설정
|
||||
- [x] 7개 목데이터 설정 완료
|
||||
|
||||
### Phase 6: 테스트 URL 문서 업데이트
|
||||
- [x] `claudedocs/[REF] juil-pages-test-urls.md` 업데이트
|
||||
|
||||
## 파일 구조
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/[locale]/(protected)/juil/order/
|
||||
│ └── base-info/
|
||||
│ └── pricing/
|
||||
│ └── page.tsx
|
||||
└── components/business/juil/
|
||||
└── pricing-management/
|
||||
├── index.ts
|
||||
├── types.ts
|
||||
├── actions.ts
|
||||
└── PricingListClient.tsx
|
||||
```
|
||||
|
||||
## 진행 상태
|
||||
- 생성일: 2026-01-05
|
||||
- 상태: ✅ 완료 (목데이터 기반)
|
||||
- 남은 작업: API 연동 시 실제 데이터 연결
|
||||
@@ -0,0 +1,117 @@
|
||||
# Phase 2.2 거래처관리 API 연동
|
||||
|
||||
**날짜**: 2026-01-09
|
||||
**작업**: 거래처관리 Mock → API 연동
|
||||
|
||||
## 개요
|
||||
|
||||
시공사 페이지 API 연동 계획 Phase 2.2 - 거래처관리(partners) API 연동 완료.
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### Backend (API)
|
||||
|
||||
#### 1. 서비스 (ClientService.php)
|
||||
- `stats()` - 거래처 통계 조회 (신규)
|
||||
- total: 전체 거래처 수
|
||||
- sales: 판매 거래처 (client_type='SALES')
|
||||
- purchase: 구매 거래처 (client_type='PURCHASE')
|
||||
- both: 판매/구매 거래처 (client_type='BOTH')
|
||||
- badDebt: 악성채권 보유 거래처 수
|
||||
- normal: 정상 거래처 수
|
||||
- `bulkDestroy()` - 일괄 삭제 (신규)
|
||||
- 주문 존재 시 해당 거래처는 건너뜀
|
||||
|
||||
#### 2. 컨트롤러 (ClientController.php)
|
||||
- `stats()` - GET /api/v1/clients/stats
|
||||
- `bulkDestroy()` - DELETE /api/v1/clients/bulk
|
||||
|
||||
#### 3. 라우트 (api.php)
|
||||
```php
|
||||
Route::get('/stats', [ClientController::class, 'stats']);
|
||||
Route::delete('/bulk', [ClientController::class, 'bulkDestroy']);
|
||||
```
|
||||
|
||||
### Frontend (React)
|
||||
|
||||
#### 1. actions.ts
|
||||
- Mock 데이터 제거 (mockPartners 배열)
|
||||
- API 연동 구현
|
||||
- `getPartnerList()` - GET /api/v1/clients
|
||||
- `getPartner()` - GET /api/v1/clients/{id}
|
||||
- `createPartner()` - POST /api/v1/clients
|
||||
- `updatePartner()` - PUT /api/v1/clients/{id}
|
||||
- `getPartnerStats()` - GET /api/v1/clients/stats
|
||||
- `deletePartner()` - DELETE /api/v1/clients/{id}
|
||||
- `deletePartners()` - DELETE /api/v1/clients/bulk
|
||||
|
||||
#### 2. 변환 함수
|
||||
- `transformClientType()` - client_type → partnerType 변환
|
||||
- `transformPartnerType()` - partnerType → client_type 변환
|
||||
- `transformPartner()` - API 응답 → Partner 타입 변환
|
||||
- `transformPartnerToApi()` - PartnerFormData → API 요청 데이터 변환
|
||||
|
||||
## API 매핑
|
||||
|
||||
| Frontend | Backend | 비고 |
|
||||
|----------|---------|------|
|
||||
| id | id | string ↔ int |
|
||||
| partnerCode | client_code | 자동 생성 |
|
||||
| businessNumber | business_no | |
|
||||
| partnerName | name | |
|
||||
| representative | contact_person | |
|
||||
| partnerType | client_type | sales/SALES, purchase/PURCHASE, both/BOTH |
|
||||
| businessType | business_type | |
|
||||
| businessCategory | business_item | |
|
||||
| address1 | address | |
|
||||
| phone | phone | |
|
||||
| mobile | mobile | |
|
||||
| fax | fax | |
|
||||
| email | email | |
|
||||
| manager | manager_name | |
|
||||
| managerPhone | manager_tel | |
|
||||
| systemManager | system_manager | |
|
||||
| outstandingAmount | outstanding_amount | 계산 필드 (매출-입금) |
|
||||
| overdueToggle | is_overdue | |
|
||||
| isBadDebt | has_bad_debt | 계산 필드 |
|
||||
| isActive | is_active | |
|
||||
| createdAt | created_at | |
|
||||
| updatedAt | updated_at | |
|
||||
|
||||
### Frontend 전용 필드 (기본값 사용)
|
||||
- zipCode, address2: ''
|
||||
- logoUrl, logoBlob: null
|
||||
- salesPaymentDay, paymentDay: 0
|
||||
- creditRating, transactionGrade: ''
|
||||
- memos, documents: []
|
||||
- category: ''
|
||||
- overdueDays: is_overdue ? 30 : 0
|
||||
|
||||
## 설계 결정
|
||||
|
||||
### 기존 Client API 재사용
|
||||
- `/api/v1/clients` 기존 엔드포인트 확장 사용
|
||||
- 별도의 `/api/v1/construction/partners` 생성하지 않음
|
||||
- accounting/vendors 와 construction/partners 모두 Client API 사용
|
||||
|
||||
### 악성채권 통계
|
||||
- BadDebt 테이블과 연계하여 악성채권 보유 거래처 수 계산
|
||||
- 상태가 '추심중' 또는 '법적조치'인 활성 악성채권만 카운트
|
||||
|
||||
### 필터링 전략
|
||||
- 검색(`q`): API에서 처리 (name, client_code, contact_person)
|
||||
- 악성채권 필터: 프론트엔드에서 처리 (API 전체 반환 후 필터)
|
||||
- 정렬: 프론트엔드에서 처리 (API 기본 정렬 사용)
|
||||
|
||||
## 진행률
|
||||
|
||||
시공사 API 연동: 4/9 (44%)
|
||||
- [x] Phase 1.1 견적관리
|
||||
- [x] Phase 1.2 인수인계보고서관리
|
||||
- [x] Phase 2.1 현장관리
|
||||
- [x] Phase 2.2 거래처관리 ← 현재 완료
|
||||
- [ ] Phase 2.3 자재관리
|
||||
- [ ] Phase 3.1 발주관리
|
||||
- [ ] Phase 3.2 재고관리
|
||||
- [ ] Phase 4.1 정산관리
|
||||
- [ ] Phase 4.2 급여관리
|
||||
@@ -0,0 +1,90 @@
|
||||
# Phase 2.1 현장관리 API 연동
|
||||
|
||||
**날짜**: 2026-01-09
|
||||
**작업**: 현장관리 Mock → API 연동
|
||||
|
||||
## 개요
|
||||
|
||||
시공사 페이지 API 연동 계획 Phase 2.1 - 현장관리(site-management) API 연동 완료.
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### Backend (API)
|
||||
|
||||
#### 1. 마이그레이션
|
||||
- `2026_01_09_162534_add_construction_fields_to_sites_table.php`
|
||||
- `site_code` (VARCHAR 50) - 현장코드
|
||||
- `client_id` (FK → clients) - 거래처 연결
|
||||
- `status` (ENUM) - unregistered/suspended/active/pending
|
||||
- 인덱스: tenant_id + site_code, tenant_id + status
|
||||
|
||||
#### 2. 모델 (Site.php)
|
||||
- 상태 상수 추가: STATUS_UNREGISTERED, STATUS_SUSPENDED, STATUS_ACTIVE, STATUS_PENDING
|
||||
- fillable 확장: site_code, client_id, status
|
||||
- Client 관계 추가
|
||||
|
||||
#### 3. 서비스 (SiteService.php)
|
||||
- `index()` - 필터 확장 (status, client_id, start_date, end_date)
|
||||
- `stats()` - 상태별 통계 조회 (신규)
|
||||
- `bulkDestroy()` - 일괄 삭제 (신규)
|
||||
|
||||
#### 4. 컨트롤러 (SiteController.php)
|
||||
- `stats()` - GET /api/v1/sites/stats
|
||||
- `bulkDestroy()` - DELETE /api/v1/sites/bulk
|
||||
|
||||
#### 5. 라우트 (api.php)
|
||||
```php
|
||||
Route::get('/stats', [SiteController::class, 'stats']);
|
||||
Route::delete('/bulk', [SiteController::class, 'bulkDestroy']);
|
||||
```
|
||||
|
||||
### Frontend (React)
|
||||
|
||||
#### 1. types.ts
|
||||
- SiteStats에 suspended, pending 필드 추가
|
||||
|
||||
#### 2. actions.ts
|
||||
- Mock 데이터 제거
|
||||
- API 연동 구현
|
||||
- `getSiteList()` - GET /api/v1/sites
|
||||
- `getSiteStats()` - GET /api/v1/sites/stats
|
||||
- `deleteSite()` - DELETE /api/v1/sites/{id}
|
||||
- `deleteSites()` - DELETE /api/v1/sites/bulk
|
||||
|
||||
## API 매핑
|
||||
|
||||
| Frontend | Backend | 비고 |
|
||||
|----------|---------|------|
|
||||
| id | id | string ↔ int |
|
||||
| siteCode | site_code | |
|
||||
| partnerId | client_id | |
|
||||
| partnerName | client.name | 관계 eager load |
|
||||
| siteName | name | |
|
||||
| address | address | |
|
||||
| status | status | 동일 |
|
||||
| createdAt | created_at | |
|
||||
| updatedAt | updated_at | |
|
||||
|
||||
## 설계 결정
|
||||
|
||||
### is_active vs status
|
||||
- `is_active` (boolean): 사용 여부 (활성화/비활성화)
|
||||
- `status` (enum): 상태값 (미등록/중지/사용/보류)
|
||||
- 두 필드는 다른 용도로 둘 다 유지
|
||||
|
||||
### 기존 API 활용
|
||||
- `/api/v1/sites` 기존 엔드포인트 확장 사용
|
||||
- `/api/v1/construction/sites` 별도 생성하지 않음
|
||||
|
||||
## 진행률
|
||||
|
||||
시공사 API 연동: 3/9 (33%)
|
||||
- [x] Phase 1.1 견적관리
|
||||
- [x] Phase 1.2 인수인계보고서관리
|
||||
- [x] Phase 2.1 현장관리 ← 현재 완료
|
||||
- [ ] Phase 2.2 거래처관리
|
||||
- [ ] Phase 2.3 자재관리
|
||||
- [ ] Phase 3.1 발주관리
|
||||
- [ ] Phase 3.2 재고관리
|
||||
- [ ] Phase 4.1 정산관리
|
||||
- [ ] Phase 4.2 급여관리
|
||||
@@ -0,0 +1,52 @@
|
||||
# 프로젝트 실행관리 상세 페이지 구현 체크리스트
|
||||
|
||||
## 구현 일자: 2026-01-12
|
||||
|
||||
## 페이지 구조
|
||||
- 페이지 경로: `/construction/project/management/[id]`
|
||||
- 칸반 보드 형태의 상세 페이지
|
||||
- 프로젝트 → 단계 → 상세 연동
|
||||
|
||||
---
|
||||
|
||||
## 작업 목록
|
||||
|
||||
### 1. 타입 및 데이터 준비
|
||||
- [x] types.ts - 상세 페이지용 타입 추가 (Stage, StageDetail, ProjectDetail 등)
|
||||
- [x] actions.ts - 상세 페이지 목업 데이터 추가
|
||||
|
||||
### 2. 칸반 보드 컴포넌트
|
||||
- [x] ProjectKanbanBoard.tsx - 칸반 보드 컨테이너
|
||||
- [x] KanbanColumn.tsx - 칸반 컬럼 공통 컴포넌트
|
||||
- [x] ProjectCard.tsx - 프로젝트 카드 (진행률, 계약금, 기간)
|
||||
- [x] StageCard.tsx - 단계 카드 (입찰/계약/시공)
|
||||
- [x] DetailCard.tsx - 상세 카드 (현장설명회 등 단순 목록)
|
||||
|
||||
### 3. 프로젝트 종료 팝업
|
||||
- [x] ProjectEndDialog.tsx - 프로젝트 종료 다이얼로그
|
||||
|
||||
### 4. 메인 페이지 조립
|
||||
- [x] ProjectDetailClient.tsx - 메인 클라이언트 컴포넌트
|
||||
- [x] page.tsx - 상세 페이지 진입점
|
||||
|
||||
### 5. 검증
|
||||
- [ ] 칸반 보드 동작 확인 (프로젝트→단계→상세 연동)
|
||||
- [ ] 프로젝트 종료 팝업 동작 확인
|
||||
- [ ] 리스트 페이지에서 상세 페이지 이동 확인
|
||||
|
||||
---
|
||||
|
||||
## 참고 사항
|
||||
- 1차 구현: 상세 하위 목록 없는 경우 (현장설명회) 먼저 구현
|
||||
- 이후 추가로 보면서 맞춰가기
|
||||
- 기존 리스트 페이지 패턴 참고
|
||||
|
||||
---
|
||||
|
||||
## 진행 상황
|
||||
- 시작: 2026-01-12
|
||||
- 현재 상태: 1차 구현 완료, 브라우저 검증 대기
|
||||
|
||||
## 테스트 URL
|
||||
- 리스트 페이지: http://localhost:3000/ko/construction/project/management
|
||||
- 상세 페이지: http://localhost:3000/ko/construction/project/management/1
|
||||
@@ -0,0 +1,231 @@
|
||||
# EstimateDetailForm.tsx 파일 분할 계획서
|
||||
|
||||
## 현황 분석
|
||||
|
||||
- **파일 위치**: `src/components/business/juil/estimates/EstimateDetailForm.tsx`
|
||||
- **현재 라인 수**: 2,088줄
|
||||
- **문제점**: 단일 파일에 모든 섹션, 핸들러, 상태 관리가 집중되어 유지보수 어려움
|
||||
|
||||
## 파일 구조 분석
|
||||
|
||||
### 현재 구조 (라인 범위)
|
||||
|
||||
| 구분 | 라인 | 설명 |
|
||||
|------|------|------|
|
||||
| Imports | 1-56 | React, UI 컴포넌트, 타입 |
|
||||
| 상수/유틸 | 58-75 | MOCK_MATERIALS, MOCK_EXPENSES, formatAmount |
|
||||
| Props | 77-81 | EstimateDetailFormProps |
|
||||
| State | 88-127 | formData, 로딩, 다이얼로그, 모달 상태 |
|
||||
| 핸들러 - 네비게이션 | 130-140 | handleBack, handleEdit, handleCancel |
|
||||
| 핸들러 - 저장/삭제 | 143-182 | handleSave, handleConfirmSave, handleDelete, handleConfirmDelete |
|
||||
| 핸들러 - 견적 요약 | 185-227 | handleAddSummaryItem, handleRemoveSummaryItem, handleSummaryItemChange |
|
||||
| 핸들러 - 공과 상세 | 230-259 | handleAddExpenseItem, handleRemoveExpenseItem, handleExpenseItemChange |
|
||||
| 핸들러 - 단가 조정 | 262-283 | handlePriceAdjustmentChange |
|
||||
| 핸들러 - 견적 상세 | 286-343 | handleAddDetailItem, handleRemoveDetailItem, handleDetailItemChange |
|
||||
| 핸들러 - 파일 업로드 | 346-435 | handleDocumentUpload, handleDocumentRemove, 드래그앤드롭 |
|
||||
| useMemo | 438-482 | pageTitle, pageDescription, headerActions |
|
||||
| JSX - 견적 정보 | 496-526 | 견적 정보 Card |
|
||||
| JSX - 현장설명회 | 528-551 | 현장설명회 정보 Card |
|
||||
| JSX - 입찰 정보 | 553-736 | 입찰 정보 Card + 파일 업로드 |
|
||||
| JSX - 견적 요약 | 738-890 | 견적 요약 정보 Table |
|
||||
| JSX - 공과 상세 | 892-1071 | 공과 상세 Table |
|
||||
| JSX - 단가 조정 | 1073-1224 | 품목 단가 조정 Table |
|
||||
| JSX - 견적 상세 | 1226-2017 | 견적 상세 Table (가장 큰 섹션) |
|
||||
| 모달/다이얼로그 | 2020-2085 | 전자결재, 견적서, 삭제/저장 다이얼로그 |
|
||||
|
||||
---
|
||||
|
||||
## 분할 계획
|
||||
|
||||
### 1단계: 섹션 컴포넌트 분리
|
||||
|
||||
```
|
||||
src/components/business/juil/estimates/
|
||||
├── EstimateDetailForm.tsx # 메인 컴포넌트 (축소)
|
||||
├── sections/
|
||||
│ ├── index.ts # 섹션 export
|
||||
│ ├── EstimateInfoSection.tsx # 견적 정보 + 현장설명회 + 입찰 정보
|
||||
│ ├── EstimateSummarySection.tsx # 견적 요약 정보
|
||||
│ ├── ExpenseDetailSection.tsx # 공과 상세
|
||||
│ ├── PriceAdjustmentSection.tsx # 품목 단가 조정
|
||||
│ └── EstimateDetailTableSection.tsx # 견적 상세 테이블
|
||||
├── hooks/
|
||||
│ ├── index.ts # hooks export
|
||||
│ └── useEstimateCalculations.ts # 계산 로직 (면적, 무게, 단가 등)
|
||||
└── utils/
|
||||
├── index.ts # utils export
|
||||
├── constants.ts # MOCK_MATERIALS, MOCK_EXPENSES
|
||||
└── formatters.ts # formatAmount
|
||||
```
|
||||
|
||||
### 2단계: 각 파일 상세
|
||||
|
||||
#### 2.1 constants.ts (~20줄)
|
||||
```typescript
|
||||
// MOCK_MATERIALS, MOCK_EXPENSES 이동
|
||||
export const MOCK_MATERIALS = [...];
|
||||
export const MOCK_EXPENSES = [...];
|
||||
```
|
||||
|
||||
#### 2.2 formatters.ts (~10줄)
|
||||
```typescript
|
||||
// formatAmount 함수 이동
|
||||
export function formatAmount(amount: number): string { ... }
|
||||
```
|
||||
|
||||
#### 2.3 useEstimateCalculations.ts (~100줄)
|
||||
```typescript
|
||||
// 견적 상세 테이블의 계산 로직 분리
|
||||
// - 면적, 무게, 철제스크린, 코킹, 레일, 하장 등 계산
|
||||
// - 합계 계산 로직
|
||||
export function useEstimateCalculations(
|
||||
item: EstimateDetailItem,
|
||||
priceAdjustmentData: PriceAdjustmentData,
|
||||
useAdjustedPrice: boolean
|
||||
) { ... }
|
||||
|
||||
export function calculateTotals(
|
||||
items: EstimateDetailItem[],
|
||||
priceAdjustmentData: PriceAdjustmentData,
|
||||
useAdjustedPrice: boolean
|
||||
) { ... }
|
||||
```
|
||||
|
||||
#### 2.4 EstimateInfoSection.tsx (~250줄)
|
||||
```typescript
|
||||
// 견적 정보 + 현장설명회 + 입찰 정보 Card 3개
|
||||
// 파일 업로드 영역 포함
|
||||
interface EstimateInfoSectionProps {
|
||||
formData: EstimateDetailFormData;
|
||||
setFormData: React.Dispatch<React.SetStateAction<EstimateDetailFormData>>;
|
||||
isViewMode: boolean;
|
||||
documentInputRef: React.RefObject<HTMLInputElement>;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.5 EstimateSummarySection.tsx (~200줄)
|
||||
```typescript
|
||||
// 견적 요약 정보 테이블
|
||||
interface EstimateSummarySectionProps {
|
||||
summaryItems: EstimateSummaryItem[];
|
||||
summaryMemo: string;
|
||||
isViewMode: boolean;
|
||||
onAddItem: () => void;
|
||||
onRemoveItem: (id: string) => void;
|
||||
onItemChange: (id: string, field: keyof EstimateSummaryItem, value: string | number) => void;
|
||||
onMemoChange: (memo: string) => void;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.6 ExpenseDetailSection.tsx (~200줄)
|
||||
```typescript
|
||||
// 공과 상세 테이블
|
||||
interface ExpenseDetailSectionProps {
|
||||
expenseItems: ExpenseItem[];
|
||||
isViewMode: boolean;
|
||||
onAddItems: (count: number) => void;
|
||||
onRemoveSelected: () => void;
|
||||
onItemChange: (id: string, field: keyof ExpenseItem, value: string | number) => void;
|
||||
onSelectItem: (id: string, selected: boolean) => void;
|
||||
onSelectAll: (selected: boolean) => void;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.7 PriceAdjustmentSection.tsx (~200줄)
|
||||
```typescript
|
||||
// 품목 단가 조정 테이블
|
||||
interface PriceAdjustmentSectionProps {
|
||||
priceAdjustmentData: PriceAdjustmentData;
|
||||
isViewMode: boolean;
|
||||
onPriceChange: (key: string, value: number) => void;
|
||||
onSave: () => void;
|
||||
onApplyAll: () => void;
|
||||
onReset: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.8 EstimateDetailTableSection.tsx (~600줄)
|
||||
```typescript
|
||||
// 견적 상세 테이블 (가장 큰 섹션)
|
||||
interface EstimateDetailTableSectionProps {
|
||||
detailItems: EstimateDetailItem[];
|
||||
priceAdjustmentData: PriceAdjustmentData;
|
||||
useAdjustedPrice: boolean;
|
||||
isViewMode: boolean;
|
||||
onAddItems: (count: number) => void;
|
||||
onRemoveItem: (id: string) => void;
|
||||
onRemoveSelected: () => void;
|
||||
onItemChange: (id: string, field: keyof EstimateDetailItem, value: string | number) => void;
|
||||
onSelectItem: (id: string, selected: boolean) => void;
|
||||
onSelectAll: (selected: boolean) => void;
|
||||
onApplyAdjustedPrice: () => void;
|
||||
onReset: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 분할 후 예상 라인 수
|
||||
|
||||
| 파일 | 예상 라인 수 |
|
||||
|------|-------------|
|
||||
| EstimateDetailForm.tsx (메인) | ~300줄 |
|
||||
| EstimateInfoSection.tsx | ~250줄 |
|
||||
| EstimateSummarySection.tsx | ~200줄 |
|
||||
| ExpenseDetailSection.tsx | ~200줄 |
|
||||
| PriceAdjustmentSection.tsx | ~200줄 |
|
||||
| EstimateDetailTableSection.tsx | ~600줄 |
|
||||
| useEstimateCalculations.ts | ~100줄 |
|
||||
| constants.ts | ~20줄 |
|
||||
| formatters.ts | ~10줄 |
|
||||
| **총합** | ~1,880줄 (약 10% 감소) |
|
||||
|
||||
---
|
||||
|
||||
## 실행 순서
|
||||
|
||||
### Phase 1: 유틸리티 분리 (5분)
|
||||
- [ ] `utils/constants.ts` 생성
|
||||
- [ ] `utils/formatters.ts` 생성
|
||||
- [ ] `utils/index.ts` 생성
|
||||
|
||||
### Phase 2: 계산 로직 분리 (10분)
|
||||
- [ ] `hooks/useEstimateCalculations.ts` 생성
|
||||
- [ ] `hooks/index.ts` 생성
|
||||
|
||||
### Phase 3: 섹션 컴포넌트 분리 (30분)
|
||||
- [ ] `sections/EstimateInfoSection.tsx` 생성
|
||||
- [ ] `sections/EstimateSummarySection.tsx` 생성
|
||||
- [ ] `sections/ExpenseDetailSection.tsx` 생성
|
||||
- [ ] `sections/PriceAdjustmentSection.tsx` 생성
|
||||
- [ ] `sections/EstimateDetailTableSection.tsx` 생성
|
||||
- [ ] `sections/index.ts` 생성
|
||||
|
||||
### Phase 4: 메인 컴포넌트 리팩토링 (10분)
|
||||
- [ ] EstimateDetailForm.tsx에서 분리된 컴포넌트 import
|
||||
- [ ] 핸들러 정리 및 props 전달
|
||||
- [ ] 불필요한 코드 제거
|
||||
|
||||
### Phase 5: 검증 (5분)
|
||||
- [ ] TypeScript 빌드 확인
|
||||
- [ ] 기능 동작 확인
|
||||
|
||||
---
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **상태 관리**: formData, setFormData는 메인 컴포넌트에서 관리, 섹션에 props로 전달
|
||||
2. **타입 일관성**: 기존 types.ts의 타입 그대로 사용
|
||||
3. **핸들러 위치**: 핸들러는 메인 컴포넌트에 유지, 섹션에 콜백으로 전달
|
||||
4. **조정단가 상태**: appliedPrices, useAdjustedPrice는 메인 컴포넌트에서 관리
|
||||
|
||||
---
|
||||
|
||||
## 5가지 수정사항 (분할 후 진행)
|
||||
|
||||
| # | 항목 | 수정 위치 (분할 후) |
|
||||
|---|------|-------------------|
|
||||
| 2 | 품목 단가 초기화 → 품목 단가만 | PriceAdjustmentSection.tsx |
|
||||
| 3 | 견적 상세 인풋 필드 추가 | EstimateDetailTableSection.tsx |
|
||||
| 4 | 견적 상세 초기화 버튼 수정 | EstimateDetailTableSection.tsx |
|
||||
| 5 | 각 섹션별 초기화 분리 | 각 Section 컴포넌트 |
|
||||
@@ -0,0 +1,292 @@
|
||||
# OrderDetailForm.tsx 분리 계획서
|
||||
|
||||
**생성일**: 2026-01-05
|
||||
**현재 파일 크기**: 1,273줄
|
||||
**목표**: 유지보수성 향상을 위한 컴포넌트 분리
|
||||
|
||||
---
|
||||
|
||||
## 현재 파일 구조 분석
|
||||
|
||||
| 영역 | 라인 | 비율 | 내용 |
|
||||
|------|------|------|------|
|
||||
| Import & Types | 1-69 | 5% | 의존성 및 타입 import |
|
||||
| Props Interface | 70-74 | 0.5% | 컴포넌트 props |
|
||||
| State & Hooks | 76-113 | 3% | 상태 관리 (12개 useState) |
|
||||
| Handlers | 114-433 | 25% | 핸들러 함수들 (20+개) |
|
||||
| JSX Render | 435-1271 | 66% | UI 렌더링 |
|
||||
|
||||
### 주요 핸들러 분류 (114-433줄)
|
||||
- **Navigation**: handleBack, handleEdit, handleCancel (114-125)
|
||||
- **Form Field**: handleFieldChange (127-133)
|
||||
- **CRUD Operations**: handleSave, handleDelete, handleDuplicate (135-199)
|
||||
- **Category Operations**: handleAddCategory, handleDeleteCategory, handleCategoryChange (206-247)
|
||||
- **Item Operations**: handleAddItems, handleDeleteSelectedItems, handleDeleteAllItems, handleItemChange (249-327)
|
||||
- **Selection**: handleToggleSelection, handleToggleSelectAll (330-357)
|
||||
- **Calendar**: handleCalendarDateClick, handleCalendarMonthChange (359-385)
|
||||
|
||||
### 주요 JSX 영역 (435-1271줄)
|
||||
- **발주 정보 Card**: 447-559 (112줄)
|
||||
- **계약 정보 Card**: 561-694 (133줄)
|
||||
- **발주 스케줄 Calendar**: 696-715 (19줄)
|
||||
- **발주 상세 테이블**: 717-1172 (455줄) ⚠️ **가장 큰 부분**
|
||||
- **카테고리 추가 버튼**: 1174-1182 (8줄)
|
||||
- **비고 Card**: 1184-1198 (14줄)
|
||||
- **Dialogs**: 1201-1261 (60줄)
|
||||
- **Document Modal**: 1263-1270 (7줄)
|
||||
|
||||
---
|
||||
|
||||
## 분리 계획
|
||||
|
||||
### Phase 1: 커스텀 훅 분리
|
||||
|
||||
**파일**: `hooks/useOrderDetailForm.ts`
|
||||
**예상 크기**: ~250줄
|
||||
|
||||
```typescript
|
||||
// 추출할 내용
|
||||
- formData 상태 관리
|
||||
- selectedItems, addCounts, categoryFilters 상태
|
||||
- calendarDate, selectedCalendarDate 상태
|
||||
- 모든 핸들러 함수들
|
||||
- calendarEvents useMemo
|
||||
```
|
||||
|
||||
**장점**:
|
||||
- 비즈니스 로직과 UI 분리
|
||||
- 테스트 용이성 향상
|
||||
- 재사용 가능
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: 카드 컴포넌트 분리
|
||||
|
||||
#### 2-1. `cards/OrderInfoCard.tsx`
|
||||
**예상 크기**: ~120줄
|
||||
|
||||
```typescript
|
||||
interface OrderInfoCardProps {
|
||||
formData: OrderDetailFormData;
|
||||
isViewMode: boolean;
|
||||
onFieldChange: (field: keyof OrderDetailFormData, value: any) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**포함 내용**: 발주번호, 발주일, 구분, 상태, 발주담당자, 화물도착지
|
||||
|
||||
---
|
||||
|
||||
#### 2-2. `cards/ContractInfoCard.tsx`
|
||||
**예상 크기**: ~150줄
|
||||
|
||||
```typescript
|
||||
interface ContractInfoCardProps {
|
||||
formData: OrderDetailFormData;
|
||||
isViewMode: boolean;
|
||||
isEditMode: boolean;
|
||||
onFieldChange: (field: keyof OrderDetailFormData, value: any) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**포함 내용**: 거래처명, 현장명, 계약번호, 공사PM, 공사담당자
|
||||
|
||||
---
|
||||
|
||||
#### 2-3. `cards/OrderScheduleCard.tsx`
|
||||
**예상 크기**: ~50줄
|
||||
|
||||
```typescript
|
||||
interface OrderScheduleCardProps {
|
||||
events: ScheduleEvent[];
|
||||
currentDate: Date;
|
||||
selectedDate: Date | null;
|
||||
onDateClick: (date: Date) => void;
|
||||
onMonthChange: (date: Date) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**포함 내용**: ScheduleCalendar 래핑
|
||||
|
||||
---
|
||||
|
||||
#### 2-4. `cards/OrderMemoCard.tsx`
|
||||
**예상 크기**: ~40줄
|
||||
|
||||
```typescript
|
||||
interface OrderMemoCardProps {
|
||||
memo: string;
|
||||
isViewMode: boolean;
|
||||
onMemoChange: (value: string) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**포함 내용**: 비고 Textarea
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: 테이블 컴포넌트 분리 (가장 중요)
|
||||
|
||||
#### 3-1. `tables/OrderDetailItemTable.tsx`
|
||||
**예상 크기**: ~350줄
|
||||
|
||||
```typescript
|
||||
interface OrderDetailItemTableProps {
|
||||
category: OrderDetailCategory;
|
||||
isEditMode: boolean;
|
||||
isViewMode: boolean;
|
||||
selectedItems: Set<string>;
|
||||
addCount: number;
|
||||
onAddCountChange: (count: number) => void;
|
||||
onAddItems: (count: number) => void;
|
||||
onDeleteSelectedItems: () => void;
|
||||
onDeleteAllItems: () => void;
|
||||
onCategoryChange: (field: keyof OrderDetailCategory, value: string) => void;
|
||||
onItemChange: (itemId: string, field: keyof OrderDetailItem, value: any) => void;
|
||||
onToggleSelection: (itemId: string) => void;
|
||||
onToggleSelectAll: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
**포함 내용**:
|
||||
- 카드 헤더 (왼쪽: 발주 상세/N건 선택/삭제, 오른쪽: 숫자/추가/카테고리/🗑️)
|
||||
- 테이블 전체 (TableHeader + TableBody)
|
||||
- 합계 행
|
||||
|
||||
---
|
||||
|
||||
#### 3-2. `tables/OrderDetailItemRow.tsx` (선택적)
|
||||
**예상 크기**: ~150줄
|
||||
|
||||
```typescript
|
||||
interface OrderDetailItemRowProps {
|
||||
item: OrderDetailItem;
|
||||
index: number;
|
||||
isEditMode: boolean;
|
||||
isSelected: boolean;
|
||||
onItemChange: (field: keyof OrderDetailItem, value: any) => void;
|
||||
onToggleSelection: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
**포함 내용**: 단일 테이블 행 렌더링
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: 다이얼로그 분리
|
||||
|
||||
#### 4-1. `dialogs/OrderDialogs.tsx`
|
||||
**예상 크기**: ~80줄
|
||||
|
||||
```typescript
|
||||
interface OrderDialogsProps {
|
||||
// 저장 다이얼로그
|
||||
showSaveDialog: boolean;
|
||||
onSaveDialogChange: (open: boolean) => void;
|
||||
onConfirmSave: () => void;
|
||||
// 삭제 다이얼로그
|
||||
showDeleteDialog: boolean;
|
||||
onDeleteDialogChange: (open: boolean) => void;
|
||||
onConfirmDelete: () => void;
|
||||
// 카테고리 삭제 다이얼로그
|
||||
showCategoryDeleteDialog: string | null;
|
||||
onCategoryDeleteDialogChange: (categoryId: string | null) => void;
|
||||
onConfirmDeleteCategory: () => void;
|
||||
// 공통
|
||||
isLoading: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 분리 후 예상 구조
|
||||
|
||||
```
|
||||
src/components/business/juil/order-management/
|
||||
├── OrderDetailForm.tsx (~200줄, 메인 컴포넌트)
|
||||
├── hooks/
|
||||
│ └── useOrderDetailForm.ts (~250줄, 비즈니스 로직)
|
||||
├── cards/
|
||||
│ ├── OrderInfoCard.tsx (~120줄)
|
||||
│ ├── ContractInfoCard.tsx (~150줄)
|
||||
│ ├── OrderScheduleCard.tsx (~50줄)
|
||||
│ └── OrderMemoCard.tsx (~40줄)
|
||||
├── tables/
|
||||
│ ├── OrderDetailItemTable.tsx (~350줄)
|
||||
│ └── OrderDetailItemRow.tsx (~150줄, 선택적)
|
||||
├── dialogs/
|
||||
│ └── OrderDialogs.tsx (~80줄)
|
||||
├── modals/
|
||||
│ └── OrderDocumentModal.tsx (기존)
|
||||
├── actions.ts (기존)
|
||||
└── types.ts (기존)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 분리 전후 비교
|
||||
|
||||
| 지표 | Before | After |
|
||||
|------|--------|-------|
|
||||
| 메인 파일 크기 | 1,273줄 | ~200줄 |
|
||||
| 가장 큰 파일 | 1,273줄 | ~350줄 |
|
||||
| 파일 개수 | 1 | 8-9 |
|
||||
| 테스트 용이성 | 낮음 | 높음 |
|
||||
| 재사용성 | 낮음 | 중간 |
|
||||
|
||||
---
|
||||
|
||||
## 실행 체크리스트
|
||||
|
||||
### Phase 1: 커스텀 훅 분리
|
||||
- [ ] `hooks/useOrderDetailForm.ts` 생성
|
||||
- [ ] 상태 변수들 이동
|
||||
- [ ] 핸들러 함수들 이동
|
||||
- [ ] useMemo 이동
|
||||
- [ ] OrderDetailForm.tsx에서 훅 사용
|
||||
|
||||
### Phase 2: 카드 컴포넌트 분리
|
||||
- [ ] `cards/OrderInfoCard.tsx` 생성
|
||||
- [ ] `cards/ContractInfoCard.tsx` 생성
|
||||
- [ ] `cards/OrderScheduleCard.tsx` 생성
|
||||
- [ ] `cards/OrderMemoCard.tsx` 생성
|
||||
- [ ] OrderDetailForm.tsx에서 import 및 사용
|
||||
|
||||
### Phase 3: 테이블 컴포넌트 분리
|
||||
- [ ] `tables/OrderDetailItemTable.tsx` 생성
|
||||
- [ ] `tables/OrderDetailItemRow.tsx` 생성 (선택적)
|
||||
- [ ] OrderDetailForm.tsx에서 import 및 사용
|
||||
|
||||
### Phase 4: 다이얼로그 분리
|
||||
- [ ] `dialogs/OrderDialogs.tsx` 생성
|
||||
- [ ] OrderDetailForm.tsx에서 import 및 사용
|
||||
|
||||
### Phase 5: 최종 검증
|
||||
- [ ] TypeScript 타입 오류 없음
|
||||
- [ ] ESLint 경고 없음
|
||||
- [ ] 빌드 성공
|
||||
- [ ] 기능 테스트 (view/edit 모드)
|
||||
- [ ] 불필요한 import 제거
|
||||
|
||||
---
|
||||
|
||||
## 우선순위 권장
|
||||
|
||||
1. **Phase 1 (Hook)** + **Phase 3 (Table)** 먼저 진행
|
||||
- 가장 큰 효과 (전체 코드의 ~60% 분리)
|
||||
- 테이블이 455줄로 가장 큼
|
||||
|
||||
2. Phase 2 (Cards) 진행
|
||||
- 추가 ~360줄 분리
|
||||
|
||||
3. Phase 4 (Dialogs) 진행
|
||||
- 마무리 정리
|
||||
|
||||
---
|
||||
|
||||
## 주의사항
|
||||
|
||||
- **타입 export**: 새 컴포넌트에서 사용할 타입들 types.ts에서 export 확인
|
||||
- **props drilling**: 너무 깊어지면 Context 고려
|
||||
- **테스트**: 분리 후 view/edit 모드 모두 테스트 필수
|
||||
- **점진적 진행**: 한 번에 모든 분리보다 단계별 진행 권장
|
||||
@@ -0,0 +1,323 @@
|
||||
# 발주관리 페이지 구현 계획서
|
||||
|
||||
> **작성일**: 2026-01-05
|
||||
> **작업 경로**: `/juil/order/order-management`
|
||||
> **상태**: ✅ 구현 완료
|
||||
|
||||
---
|
||||
|
||||
## 📋 스크린샷 분석 결과
|
||||
|
||||
### 화면 구성
|
||||
|
||||
#### 1. 상단 - 발주 스케줄 (달력 영역)
|
||||
| 요소 | 설명 |
|
||||
|------|------|
|
||||
| **뷰 전환** | 주(Week) / 월(Month) 탭 전환 |
|
||||
| **년월 네비게이션** | 2025년 12월 ◀ ▶ 버튼 |
|
||||
| **필터** | 작업반장별 필터 (이번년+8주 화살표 버튼) |
|
||||
| **일정 바(Bar)** | "담당자 - 현장명 / 발주번호" 형태로 여러 날에 걸쳐 표시 |
|
||||
| **일정 색상** | 회색(완료), 파란색(진행중) 구분 |
|
||||
| **일자 뱃지** | 빨간 원 안에 숫자 (06, 07, 08 등) - 상태/건수 표시 |
|
||||
| **더보기** | +15 형태로 해당 일자에 추가 일정 있음 표시 |
|
||||
| **달력 클릭** | 특정 일자 클릭 시 아래 리스트에 해당 일자 데이터만 필터링 |
|
||||
|
||||
#### 2. 하단 - 발주 목록 (리스트 영역)
|
||||
| 요소 | 설명 |
|
||||
|------|------|
|
||||
| **날짜 범위** | 2025-09-01 ~ 2025-09-03 형태 |
|
||||
| **빠른 필터 탭** | 당해년도 / 전년도 / 전월 / 당월 / 어제 / 오늘 |
|
||||
| **검색** | 검색창 + 건수 표시 (7건, 12건 선택) |
|
||||
| **상태 필터** | 빨간 원 숫자 버튼들 (전체/상태별) |
|
||||
| **삭제 버튼** | 선택된 항목 삭제 |
|
||||
|
||||
#### 3. 테이블 컬럼
|
||||
| 컬럼 | 설명 |
|
||||
|------|------|
|
||||
| 체크박스 | 선택 |
|
||||
| 계약일련번호 | - |
|
||||
| 거래처 | 회사명 |
|
||||
| 현장명 | 작업 현장 |
|
||||
| 병동 | - |
|
||||
| 공 | - |
|
||||
| 시APM | 담당 PM |
|
||||
| 발주번호 | 발주 식별 번호 |
|
||||
| 발주번 담자 | 발주 담당자 |
|
||||
| 발주처 | - |
|
||||
| 작업반 시공품 | 작업 내용 |
|
||||
| 기간 | 작업 기간 |
|
||||
| 구분 | 상태 구분 |
|
||||
| 실적 납품일 | 실제 납품 완료일 |
|
||||
| 납품일 | 예정 납품일 |
|
||||
|
||||
#### 4. 작업 버튼 (선택 시)
|
||||
- 수정 버튼
|
||||
- 삭제 버튼
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 구현 범위
|
||||
|
||||
### Phase 1: 공통 달력 컴포넌트 (ScheduleCalendar)
|
||||
**재사용 가능한 스케줄 달력 컴포넌트**
|
||||
|
||||
```
|
||||
src/components/common/
|
||||
└── ScheduleCalendar/
|
||||
├── index.tsx # 메인 컴포넌트
|
||||
├── ScheduleCalendar.tsx # 달력 본체
|
||||
├── CalendarHeader.tsx # 헤더 (년월/뷰전환/필터)
|
||||
├── MonthView.tsx # 월간 뷰
|
||||
├── WeekView.tsx # 주간 뷰
|
||||
├── ScheduleBar.tsx # 일정 바 컴포넌트
|
||||
├── DayCell.tsx # 일자 셀 컴포넌트
|
||||
├── MorePopover.tsx # +N 더보기 팝오버
|
||||
├── types.ts # 타입 정의
|
||||
└── utils.ts # 유틸리티 함수
|
||||
```
|
||||
|
||||
**기능 요구사항**:
|
||||
- [ ] 월간/주간 뷰 전환
|
||||
- [ ] 년월 네비게이션 (이전/다음)
|
||||
- [ ] 일정 바(Bar) 렌더링 (여러 날에 걸침)
|
||||
- [ ] 일정 색상 구분 (상태별)
|
||||
- [ ] 일자별 뱃지 숫자 표시
|
||||
- [ ] +N 더보기 기능 (3개 초과 시)
|
||||
- [ ] 일자 클릭 이벤트 콜백
|
||||
- [ ] 필터 영역 slot (외부에서 주입)
|
||||
- [ ] 반응형 디자인
|
||||
|
||||
### Phase 2: 발주관리 리스트 페이지
|
||||
**페이지 및 컴포넌트 구조**
|
||||
|
||||
```
|
||||
src/app/[locale]/(protected)/juil/order/
|
||||
└── order-management/
|
||||
└── page.tsx # 페이지 엔트리
|
||||
|
||||
src/components/business/juil/order-management/
|
||||
├── OrderManagementListClient.tsx # 메인 클라이언트 컴포넌트
|
||||
├── OrderCalendarSection.tsx # 달력 섹션 (ScheduleCalendar 사용)
|
||||
├── OrderListSection.tsx # 리스트 섹션
|
||||
├── OrderStatusFilter.tsx # 상태 필터 (빨간 원 숫자)
|
||||
├── OrderDateFilter.tsx # 날짜 빠른 필터 (당해년도/전월 등)
|
||||
├── types.ts # 타입 정의
|
||||
├── actions.ts # Server Actions
|
||||
└── index.ts # 배럴 export
|
||||
```
|
||||
|
||||
**기능 요구사항**:
|
||||
- [ ] 달력과 리스트 통합 레이아웃
|
||||
- [ ] 달력 일자 클릭 → 리스트 필터 연동
|
||||
- [ ] 날짜 범위 선택
|
||||
- [ ] 빠른 날짜 필터 (당해년도/전년도/전월/당월/어제/오늘)
|
||||
- [ ] 상태별 필터 (빨간 원 숫자 버튼)
|
||||
- [ ] 검색 기능
|
||||
- [ ] 테이블 (체크박스/정렬/페이지네이션)
|
||||
- [ ] 선택 시 작업 버튼 표시
|
||||
- [ ] 삭제 기능
|
||||
|
||||
---
|
||||
|
||||
## 📦 기술 의존성
|
||||
|
||||
### 새로 설치 필요
|
||||
```bash
|
||||
# FullCalendar 라이브러리 (또는 커스텀 구현)
|
||||
npm install @fullcalendar/core @fullcalendar/react @fullcalendar/daygrid @fullcalendar/timegrid @fullcalendar/interaction
|
||||
```
|
||||
|
||||
**대안**: FullCalendar 없이 커스텀 달력 컴포넌트로 구현
|
||||
- 장점: 번들 사이즈 감소, 완전한 커스터마이징
|
||||
- 단점: 구현 복잡도 증가
|
||||
|
||||
### 기존 사용
|
||||
- `IntegratedListTemplateV2` - 리스트 템플릿
|
||||
- `DateRangeSelector` - 날짜 범위 선택
|
||||
- `date-fns` - 날짜 유틸리티
|
||||
|
||||
---
|
||||
|
||||
## 🔧 세부 구현 체크리스트
|
||||
|
||||
### Phase 1: 공통 달력 컴포넌트 (ScheduleCalendar)
|
||||
|
||||
#### 1.1 기본 구조 및 타입 정의
|
||||
- [ ] `types.ts` 생성 (ScheduleEvent, CalendarView, CalendarProps 등)
|
||||
- [ ] `utils.ts` 생성 (날짜 계산, 일정 위치 계산 등)
|
||||
- [ ] 컴포넌트 폴더 구조 생성
|
||||
|
||||
#### 1.2 CalendarHeader 컴포넌트
|
||||
- [ ] 년월 표시 및 네비게이션 (◀ ▶)
|
||||
- [ ] 주/월 뷰 전환 탭
|
||||
- [ ] 필터 slot (children으로 외부 주입)
|
||||
|
||||
#### 1.3 MonthView 컴포넌트
|
||||
- [ ] 월간 그리드 레이아웃 (7x6)
|
||||
- [ ] 요일 헤더 (일~토)
|
||||
- [ ] 날짜 셀 렌더링
|
||||
- [ ] 이전/다음 달 날짜 표시 (opacity 처리)
|
||||
- [ ] 오늘 날짜 하이라이트
|
||||
|
||||
#### 1.4 WeekView 컴포넌트
|
||||
- [ ] 주간 그리드 레이아웃 (7 컬럼)
|
||||
- [ ] 요일 헤더 (날짜 + 요일)
|
||||
- [ ] 날짜 셀 렌더링
|
||||
|
||||
#### 1.5 DayCell 컴포넌트
|
||||
- [ ] 날짜 숫자 표시
|
||||
- [ ] 뱃지 숫자 표시 (빨간 원)
|
||||
- [ ] 클릭 이벤트 처리
|
||||
- [ ] 선택 상태 스타일
|
||||
|
||||
#### 1.6 ScheduleBar 컴포넌트
|
||||
- [ ] 일정 바 렌더링 (시작~종료 날짜)
|
||||
- [ ] 여러 날에 걸치는 바 계산 (주 단위 분할)
|
||||
- [ ] 색상 구분 (상태별)
|
||||
- [ ] 호버/클릭 이벤트
|
||||
- [ ] 텍스트 truncate 처리
|
||||
|
||||
#### 1.7 MorePopover 컴포넌트
|
||||
- [ ] +N 버튼 렌더링
|
||||
- [ ] 팝오버로 숨겨진 일정 목록 표시
|
||||
- [ ] 일정 항목 클릭 이벤트
|
||||
|
||||
#### 1.8 메인 ScheduleCalendar 컴포넌트
|
||||
- [ ] 상태 관리 (현재 월, 뷰 모드, 선택된 날짜)
|
||||
- [ ] 일정 데이터 받아서 렌더링
|
||||
- [ ] 이벤트 콜백 (onDateClick, onEventClick, onMonthChange)
|
||||
- [ ] 반응형 처리
|
||||
|
||||
### Phase 2: 발주관리 리스트 페이지
|
||||
|
||||
#### 2.1 타입 및 설정
|
||||
- [ ] `types.ts` - Order 타입, 필터 옵션, 상태 정의
|
||||
- [ ] `actions.ts` - Server Actions (목업 데이터)
|
||||
|
||||
#### 2.2 page.tsx
|
||||
- [ ] 페이지 라우트 생성
|
||||
- [ ] 메타데이터 설정
|
||||
- [ ] 클라이언트 컴포넌트 import
|
||||
|
||||
#### 2.3 OrderDateFilter 컴포넌트
|
||||
- [ ] 빠른 날짜 필터 버튼 (당해년도/전년도/전월/당월/어제/오늘)
|
||||
- [ ] 클릭 시 날짜 범위 계산
|
||||
- [ ] 활성화 상태 스타일
|
||||
|
||||
#### 2.4 OrderStatusFilter 컴포넌트
|
||||
- [ ] 상태별 필터 버튼 (빨간 원 숫자)
|
||||
- [ ] 전체/상태별 카운트 표시
|
||||
- [ ] 선택 상태 스타일
|
||||
|
||||
#### 2.5 OrderCalendarSection 컴포넌트
|
||||
- [ ] ScheduleCalendar 사용
|
||||
- [ ] 필터 영역 (작업반장 셀렉트)
|
||||
- [ ] 일자 클릭 이벤트 → 리스트 필터 연동
|
||||
- [ ] 스케줄 데이터 매핑
|
||||
|
||||
#### 2.6 OrderListSection 컴포넌트
|
||||
- [ ] IntegratedListTemplateV2 기반
|
||||
- [ ] 테이블 컬럼 정의
|
||||
- [ ] 행 렌더링 (체크박스, 데이터, 작업 버튼)
|
||||
- [ ] 선택 시 작업 버튼 표시
|
||||
- [ ] 모바일 카드 렌더링
|
||||
|
||||
#### 2.7 OrderManagementListClient 컴포넌트
|
||||
- [ ] 전체 상태 관리 (달력 + 리스트 연동)
|
||||
- [ ] 달력 일자 선택 → 리스트 필터
|
||||
- [ ] 날짜 범위 필터
|
||||
- [ ] 상태 필터
|
||||
- [ ] 검색 필터
|
||||
- [ ] 정렬
|
||||
- [ ] 페이지네이션
|
||||
- [ ] 삭제 기능
|
||||
|
||||
### Phase 3: 통합 테스트 및 마무리
|
||||
- [ ] 달력-리스트 연동 테스트
|
||||
- [ ] 반응형 테스트
|
||||
- [ ] 목업 데이터 검증
|
||||
- [ ] 테스트 URL 등록
|
||||
|
||||
---
|
||||
|
||||
## 🎨 디자인 명세
|
||||
|
||||
### 달력 색상
|
||||
| 상태 | 바 색상 | 뱃지 색상 |
|
||||
|------|---------|-----------|
|
||||
| 완료 | 회색 (`bg-gray-400`) | - |
|
||||
| 진행중 | 파란색 (`bg-blue-500`) | 빨간색 (`bg-red-500`) |
|
||||
| 대기 | 노란색 (`bg-yellow-500`) | 빨간색 (`bg-red-500`) |
|
||||
|
||||
### 레이아웃
|
||||
```
|
||||
+--------------------------------------------------+
|
||||
| 📅 발주관리 [발주 등록] |
|
||||
+--------------------------------------------------+
|
||||
| [발주 스케줄] |
|
||||
| +----------------------------------------------+ |
|
||||
| | 2025년 12월 [주] [월] [작업반장 ▼] | |
|
||||
| | ◀ ▶ | |
|
||||
| |----------------------------------------------|
|
||||
| | 일 | 월 | 화 | 수 | 목 | 금 | 토 | |
|
||||
| |----------------------------------------------|
|
||||
| | | | 1 | 2 | 3 | 4 | 5 | |
|
||||
| | 📊 | | ━━━━━━━━━━━━━━━━━━━ 일정바 ━━━━━━ | |
|
||||
| |----------------------------------------------|
|
||||
| | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
|
||||
| | ⓪ | ⓪ | | | | | | |
|
||||
| +----------------------------------------------+ |
|
||||
+--------------------------------------------------+
|
||||
| [발주 목록] |
|
||||
| +----------------------------------------------+ |
|
||||
| | 2025-09-01 ~ 2025-09-03 | |
|
||||
| | [당해년도][전년도][전월][당월][어제][오늘] | |
|
||||
| |----------------------------------------------|
|
||||
| | 🔍 검색... 7건 | ⓿ ❶ ❷ ❸ | [삭제] | |
|
||||
| |----------------------------------------------|
|
||||
| | ☐ | 번호 | 거래처 | 현장명 | ... | 작업 | |
|
||||
| | ☐ | 1 | A사 | 현장1 | ... | [버튼들] | |
|
||||
| +----------------------------------------------+ |
|
||||
+--------------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 참고사항
|
||||
|
||||
### 달력 라이브러리 선택
|
||||
**추천: 커스텀 구현**
|
||||
- FullCalendar는 기능이 과도하고 번들 사이즈가 큼
|
||||
- 스크린샷의 요구사항은 커스텀으로 충분히 구현 가능
|
||||
- `date-fns` 활용하여 날짜 계산
|
||||
|
||||
### 기존 패턴 준수
|
||||
- `IntegratedListTemplateV2` 사용
|
||||
- `DateRangeSelector` 재사용
|
||||
- `StructureReviewListClient` 패턴 참조
|
||||
|
||||
### 향후 확장
|
||||
- 다른 페이지에서 ScheduleCalendar 재사용
|
||||
- 일정 등록/수정 모달 추가 예정
|
||||
- 드래그 앤 드롭 일정 이동 (선택적)
|
||||
|
||||
---
|
||||
|
||||
## ✅ 작업 순서
|
||||
|
||||
1. **Phase 1.1-1.2**: 타입 정의 및 CalendarHeader
|
||||
2. **Phase 1.3-1.4**: MonthView / WeekView
|
||||
3. **Phase 1.5-1.6**: DayCell / ScheduleBar
|
||||
4. **Phase 1.7-1.8**: MorePopover / 메인 컴포넌트
|
||||
5. **Phase 2.1-2.2**: 발주관리 타입 및 페이지
|
||||
6. **Phase 2.3-2.4**: 날짜/상태 필터
|
||||
7. **Phase 2.5-2.6**: 달력/리스트 섹션
|
||||
8. **Phase 2.7**: 메인 클라이언트 컴포넌트
|
||||
9. **Phase 3**: 통합 테스트
|
||||
|
||||
---
|
||||
|
||||
## 🔗 관련 문서
|
||||
- `[REF] juil-project-structure.md` - 주일 프로젝트 구조
|
||||
- `StructureReviewListClient.tsx` - 리스트 패턴 참조
|
||||
- `IntegratedListTemplateV2.tsx` - 템플릿 참조
|
||||
82
claudedocs/construction/[REF] construction-project-flow.md
Normal file
82
claudedocs/construction/[REF] construction-project-flow.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Juil Project Process Flow Analysis
|
||||
Based on provided flowcharts.
|
||||
|
||||
## 1. Project Progress Flow (Main Lifecycle)
|
||||
|
||||
### Modules & Roles
|
||||
| Role | Key Activities | Output/State |
|
||||
|---|---|---|
|
||||
| **Field Briefing User** | Attend briefing, Upload data | Project Initiated |
|
||||
| **Estimate/Bid Manager** | Create Estimate (Approve/Return) <br> Bid Participation <br> Win/Loss Check | Estimate Created <br> Bid Submitted <br> Project Won/Lost |
|
||||
| **Contract Manager** | Create Contract (Approve/Return) <br> Contract Execution <br> Handover Decision | Contract Finalized |
|
||||
| **Order/Construction Manager** | Handover Creation (Approve/Return) <br> Field Measurement <br> Structural Review (if needed) <br> Order Creation (Approve/Return) <br> Construction Start | Handover Doc <br> Measurement Data <br> Structural Report <br> Order Placed |
|
||||
| **Progress Billing Manager** | Create Progress Billing (Approve/Return) <br> Change Contract Check <br> Client Approval <br> Settlement | Bill Created <br> Settlement Complete |
|
||||
|
||||
---
|
||||
|
||||
## 2. Construction & Billing Detail Flow
|
||||
|
||||
### Detailed Steps by Role
|
||||
|
||||
#### Order Manager
|
||||
1. **Handover**: Create handover document -> Approval Loop.
|
||||
2. **Field Work**: Field Measurement.
|
||||
3. **Engineering**: Structural Review (Condition: if needed).
|
||||
4. **Ordering**: Create Order -> Approval Loop.
|
||||
|
||||
#### Construction Manager
|
||||
1. **Execution**: Start Construction.
|
||||
2. **Resources**: Request Vehicles/Equipment.
|
||||
3. **Management**: Construction Management -> Issue Check.
|
||||
4. **Issue Handling**: Manage Issues if they arise.
|
||||
|
||||
#### Work Foreman (Field)
|
||||
1. **Assignment**: Receive Construction Assignment.
|
||||
2. **Personnel**: Check New Personnel -> Sign up if needed.
|
||||
3. **Attendance**: GPS Attendance Check.
|
||||
4. **Daily Work**:
|
||||
- Perform Construction Work.
|
||||
- Photo Documentation.
|
||||
- Work Report.
|
||||
- Personnel Status Report.
|
||||
|
||||
#### Progress Billing Manager
|
||||
1. **Billing**: Create Progress Billing -> Approval Loop.
|
||||
2. **Change Mgmt**: Check if Change Contract is needed.
|
||||
- If needed: Trigger Contract Manager flow.
|
||||
3. **Client**: Get Construction Company (Client) Approval.
|
||||
4. **Finish**: Settlement.
|
||||
|
||||
#### Contract Manager (Change Process)
|
||||
1. **Drafting**: Create Change Contract (triggered by Billing).
|
||||
2. **Approval**: Internal Approval Loop.
|
||||
3. **Execution**: Change Contract Process.
|
||||
4. **Client**: Get Construction Company (Client) Approval.
|
||||
5. **Finish**: Change Contract Complete.
|
||||
|
||||
---
|
||||
|
||||
## 3. Proposed Menu Structure (Juil)
|
||||
|
||||
Based on the flow, the recommended menu structure is:
|
||||
|
||||
- **Dashboard**: Overall Status
|
||||
- **Project Management** (프로젝트 관리)
|
||||
- Field Briefing (현장설명회)
|
||||
- Estimates & Bids (견적/입찰)
|
||||
- Contracts (계약관리)
|
||||
- **Construction Management** (공사관리)
|
||||
- Handovers (인수인계)
|
||||
- Field Measurements (현장실측)
|
||||
- Structural Reviews (구조검토)
|
||||
- Orders (발주관리)
|
||||
- Construction Execution (시공관리) - Includes Vehicles, Issues
|
||||
- **Field Work** (현장작업) - Mobile Optimized?
|
||||
- My Assignments (시공할당)
|
||||
- Personnel Mgmt (인력관리)
|
||||
- Attendance (GPS출근)
|
||||
- Daily Reports (업무보고/사진)
|
||||
- **Billing & Settlement** (기성/정산)
|
||||
- Progress Billing (기성청구)
|
||||
- Change Contracts (변경계약)
|
||||
- Settlements (정산관리)
|
||||
89
claudedocs/construction/[REF] juil-project-structure.md
Normal file
89
claudedocs/construction/[REF] juil-project-structure.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# 주일 공사 MES 프로젝트 구조
|
||||
|
||||
Last Updated: 2025-12-30
|
||||
|
||||
## 프로젝트 개요
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| 업체명 | 주일 |
|
||||
| 업종 | 공사 (건설/시공) |
|
||||
| 프로젝트 유형 | MES (Manufacturing Execution System) |
|
||||
| 기존 프로젝트 | 경동 (셔터 업체) |
|
||||
|
||||
## 디렉토리 구조
|
||||
|
||||
```
|
||||
src/app/[locale]/(protected)/
|
||||
├── juil/ # 주일 전용 페이지들
|
||||
│ ├── page.tsx # 메인 페이지 (예정)
|
||||
│ ├── [기능명]/ # 각 기능별 페이지
|
||||
│ └── ...
|
||||
│
|
||||
├── dev/
|
||||
│ └── juil-test-urls/ # 테스트 URL 관리 페이지
|
||||
│ ├── page.tsx # 서버 컴포넌트 (MD 파싱)
|
||||
│ └── JuilTestUrlsClient.tsx # 클라이언트 컴포넌트
|
||||
│
|
||||
└── (기존 경동 페이지들)
|
||||
```
|
||||
|
||||
## 컴포넌트 구조 (예정)
|
||||
|
||||
```
|
||||
src/components/business/juil/ # 주일 전용 비즈니스 컴포넌트
|
||||
├── common/ # 공통 컴포넌트
|
||||
├── [기능명]/ # 기능별 컴포넌트
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 테스트 URL 페이지
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| URL | http://localhost:3000/dev/juil-test-urls |
|
||||
| MD 파일 | `claudedocs/[REF] juil-pages-test-urls.md` |
|
||||
| 용도 | 개발 중인 주일 페이지 URL 관리 및 빠른 접근 |
|
||||
|
||||
### MD 파일 형식
|
||||
|
||||
```markdown
|
||||
## 카테고리명
|
||||
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| **페이지명** | `/ko/juil/...` | 상태표시 |
|
||||
```
|
||||
|
||||
## 경동 vs 주일 비교
|
||||
|
||||
| 항목 | 경동 | 주일 |
|
||||
|------|------|------|
|
||||
| 업종 | 셔터 | 공사 |
|
||||
| 경로 | `/ko/...` (기존 경로) | `/ko/juil/...` |
|
||||
| 컴포넌트 | `src/components/...` | `src/components/business/juil/...` |
|
||||
| 문서 | `claudedocs/...` | `claudedocs/juil/...` |
|
||||
|
||||
## 개발 가이드
|
||||
|
||||
### 새 페이지 추가 시
|
||||
|
||||
1. `src/app/[locale]/(protected)/juil/[기능명]/` 폴더 생성
|
||||
2. `page.tsx` 생성
|
||||
3. 필요 시 `src/components/business/juil/[기능명]/` 컴포넌트 생성
|
||||
4. `claudedocs/[REF] juil-pages-test-urls.md`에 URL 추가
|
||||
|
||||
### 테스트 URL 등록
|
||||
|
||||
`claudedocs/[REF] juil-pages-test-urls.md` 파일에 마크다운 테이블 형식으로 추가:
|
||||
|
||||
```markdown
|
||||
| **새페이지** | `/ko/juil/new-page` | NEW |
|
||||
```
|
||||
|
||||
## 관련 파일 목록
|
||||
|
||||
- `claudedocs/[REF] juil-pages-test-urls.md` - 테스트 URL 목록
|
||||
- `claudedocs/juil/` - 주일 프로젝트 문서 폴더
|
||||
- `src/app/[locale]/(protected)/juil/` - 페이지 파일
|
||||
- `src/components/business/juil/` - 컴포넌트 파일
|
||||
@@ -0,0 +1,435 @@
|
||||
# [IMPL-2026-01-07] 대표님 전용 대시보드 구현
|
||||
|
||||
## 프로젝트 개요
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| 작업명 | 대표님 전용 대시보드 (CEO Dashboard) |
|
||||
| 기준 페이지 | `/reports/comprehensive-analysis` (종합분석) |
|
||||
| 대상 페이지 | `/dashboard` (대시보드) |
|
||||
| 기존 대시보드 처리 | 백업 후 새 대시보드로 교체 |
|
||||
| 공통 컴포넌트 활용 | `ScheduleCalendar` (달력) |
|
||||
|
||||
---
|
||||
|
||||
## 작업 범위
|
||||
|
||||
### Phase 1: 본 화면 구현 (현재 작업) ✅ 완료
|
||||
- [x] 스크린샷 분석 및 계획서 작성
|
||||
- [x] 기존 Dashboard 컴포넌트 백업
|
||||
- [x] CEO Dashboard 컴포넌트 생성
|
||||
- [x] 각 섹션별 컴포넌트 구현 (11개 섹션)
|
||||
|
||||
### Phase 2: 팝업/상세 화면 구현 (추후 작업)
|
||||
- [ ] 항목 설정 팝업
|
||||
- [ ] 일일 일보 정보 팝업
|
||||
- [ ] 해당월 예상 지출 상세 팝업
|
||||
- [ ] 납부세액 내역 상세 팝업
|
||||
- [ ] 일정 상세 팝업
|
||||
- [ ] 기타 상세 팝업들
|
||||
|
||||
---
|
||||
|
||||
## 페이지 구조 (스크린샷 기준)
|
||||
|
||||
### 섹션 1: 대시보드 헤더 (Page 31 상단)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ LOGO 대시보드 - 전체 현황을 조회합니다. [항목 설정] │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| 요소 | 설명 | 클릭 동작 |
|
||||
|------|------|----------|
|
||||
| 항목 설정 버튼 | 우측 상단 | 대시보드 항목 설정 팝업 표시 |
|
||||
|
||||
---
|
||||
|
||||
### 섹션 2: 오늘의 이슈 (Page 31)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 🔴 오늘의 이슈 │
|
||||
├──────────┬──────────┬──────────┬──────────────────────┤
|
||||
│ 수주 │ 채권 추심 │ 반전 재고 │ 제규 신고 │
|
||||
│ 3건 │ 3건 │ 3건 │ 부가세 신고 D-15 │
|
||||
├──────────┼──────────┼──────────┼──────────────────────┤
|
||||
│ 신규업체 │ 연차 │ 발주 │ 결재 요청 │
|
||||
│ 등록 3건│ 3건 │ 3건 │ 3건 │
|
||||
└──────────┴──────────┴──────────┴──────────────────────┘
|
||||
```
|
||||
|
||||
| 요소 | 설명 | 클릭 동작 |
|
||||
|------|------|----------|
|
||||
| 수주 | 수주 건수 | 수주 관리 화면 이동 |
|
||||
| 채권 추심 | 채권 추심 건수 | 채권 추심 관리 화면 이동 |
|
||||
| 반전 재고 | 빨간색 강조 (위험) | 재고 관리 화면 이동 |
|
||||
| 제규 신고 | 부가세 신고 D-day | 세무 관리 화면 이동 |
|
||||
| 신규 업체 등록 | 신규 업체 건수 | 업체 관리 화면 이동 |
|
||||
| 연차 | 연차 신청 건수 | 연차 관리 화면 이동 |
|
||||
| 발주 | 발주 건수 | 발주 관리 화면 이동 |
|
||||
| 결재 요청 | 결재 대기 건수 | 결재 관리 화면 이동 |
|
||||
|
||||
---
|
||||
|
||||
### 섹션 3: 일일 일보 (Page 31)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 🔴 일일 일보 2026년 1월 5일 월요일 │
|
||||
├──────────────┬──────────────┬──────────────┬───────────┤
|
||||
│ 입금/자산 │ 전월 매출 │ (지표3) │ (지표4) │
|
||||
│ 30.5억원 │ $11,123,000 │ 10.2억원 │ 3.5억원 │
|
||||
└──────────────┴──────────────┴──────────────┴───────────┘
|
||||
│ ⚠️ 최근 7일 평균 대비 3배 이상으로 입금이 발생했습니다. │
|
||||
│ ⚠️ 102만원이 감지됐습니다... (이상거래 감지) │
|
||||
│ ℹ️ 현금성 자산이 300건전환입니다. 월 운영비와 비용보다... │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| 요소 | 설명 | 클릭 동작 |
|
||||
|------|------|----------|
|
||||
| 일일 일보 영역 전체 | 오늘 날짜 기준 일보 | 일일 일보 정보 팝업 표시 |
|
||||
|
||||
---
|
||||
|
||||
### 섹션 4: 당월 예상 지출 내역 (Page 32)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 🔴 당월 예상 지출 내역 │
|
||||
├──────────────┬──────────────┬──────────────┬───────────┤
|
||||
│ 미청산가지급금│ 이달 예상 │ 전달 대비 │ 차이 │
|
||||
│ 30.5억원 │30,123,000원 │30,123,000원 │ 3.5억원 │
|
||||
│ 전달14%,+5% │ │ │ │
|
||||
└──────────────┴──────────────┴──────────────┴───────────┘
|
||||
│ ⚠️ 이번 달 예상 지출이 전달 해당 15% 증가했습니다... │
|
||||
│ ⚠️ 이번 달 예상 지출이 예상 12% 초과했습니다... │
|
||||
│ ✅ 이번 달 예상 지출이 전달 대비 8% 감소했습니다... │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| 요소 | 설명 | 클릭 동작 |
|
||||
|------|------|----------|
|
||||
| 가지급금 | 미청산 가지급금 | 가지급금 관리 화면 이동 |
|
||||
| 미청산 가지급금 | 대상 금액 | 미청산 가지급금 상세 화면 이동 |
|
||||
| 해당월 예상 지출 | 지출 상세 | 해당월 예상 지출 상세 팝업 표시 |
|
||||
|
||||
---
|
||||
|
||||
### 섹션 5: 카드/가지급금 관리 (Page 32)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 🔴 카드/가지급금 관리 │
|
||||
├──────────────┬──────────────┬──────────────┬───────────┤
|
||||
│ 해당달 대상 │ 가지급금 │ 미정산 │ 총잔액 │
|
||||
│30,123,000원 │ 3.5억원 │3,123,000원 │3,123,000원│
|
||||
└──────────────┴──────────────┴──────────────┴───────────┘
|
||||
│ ⚠️ 법인카드 사용 총 85만원이 가지급금으로 전환됐습니다... │
|
||||
│ ⚠️ 전 가지급금 1,520만원은 4.6%, 연 약 70만원의 인정이자...│
|
||||
│ ⚠️ 상품권/귀금속 등 현대비 불인정 항목 매입 건이 있습니다 │
|
||||
│ ℹ️ 주말 카드 사용 총 100만원 중 결과 지의... │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| 요소 | 설명 | 클릭 동작 |
|
||||
|------|------|----------|
|
||||
| 법인카드 예상 가능 영역 | 카드 사용 현황 | 법인카드 관리 화면 이동 |
|
||||
|
||||
---
|
||||
|
||||
### 섹션 6: 접대비 현황 (Page 32~33)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 🔴 접대비 현황 │
|
||||
├──────────────┬──────────────┬──────────────┬───────────┤
|
||||
│ 접대비 한도 │ 접대비 사용액 │ 한도 잔액 │ 기타 │
|
||||
│ 305.3억원 │40,123,000원 │30,123,000원 │10,000,000원│
|
||||
└──────────────┴──────────────┴──────────────┴───────────┘
|
||||
│ ✅ 접대비 사용 총 2,400만원 중 / 한도 4,000만원 (60%)... │
|
||||
│ ⚠️ 접대비 85% 도달. 연내 한도 600만원 잔액입니다... │
|
||||
│ ❌ 접대비 한도 초과 320만원 발생. 손금불산입되어... │
|
||||
│ ℹ️ 접대비 사용 총 3건(45만원)이 거래처 한도 누락... │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| 요소 | 설명 | 클릭 동작 |
|
||||
|------|------|----------|
|
||||
| 접대비 영역 | 접대비 현황 | 해당월 예상 지출 상세 팝업 표시 |
|
||||
|
||||
---
|
||||
|
||||
### 섹션 7: 복리후생비 현황 (Page 33)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 🔴 복리후생비 현황 │
|
||||
├──────────────┬──────────────┬──────────────┬───────────┤
|
||||
│ 총 복리후생비│누적 사용 │ 잠정 사용액 │ 잠정 한도 │
|
||||
│30,123,000원 │10,123,000원 │ 5,123,000원 │5,123,000원│
|
||||
└──────────────┴──────────────┴──────────────┴───────────┘
|
||||
│ ✅ 1인당 월 복리후생비 18만원. 업계 평균 내 정상 운영... │
|
||||
│ ⚠️ 식대가 월 25만원으로 비과세 한도 초과... │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 섹션 8: 미수금 현황 (Page 33)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 🔴 미수금 현황 │
|
||||
├──────────────┬──────────────┬──────────────┬───────────┤
|
||||
│ 누계 미수금 │ 30일 초과 │ 60일 초과 │ 90일 초과 │
|
||||
│30,123,000원 │10,123,000원 │ 3,123,000원 │2,123,000원│
|
||||
│매출:6,012만 │매출:6,012만 │매출:6,012만 │매출:6,012만│
|
||||
└──────────────┴──────────────┴──────────────┴───────────┘
|
||||
│ ❌ 90일 이상 장기 미수금 3건(2,500만원) 발생. 회수조치... │
|
||||
│ ⚠️ (주)대한전자 미수금 4,500만원으로 전체의 35%... │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| 요소 | 설명 | 클릭 동작 |
|
||||
|------|------|----------|
|
||||
| 미수금 현황 목록 | 미수금 상세 | 미수금 상세 화면으로 이동 (1,2차 표시) |
|
||||
|
||||
---
|
||||
|
||||
### 섹션 9: 채권추심 현황 (Page 34)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 🔴 채권추심 현황 │
|
||||
├──────────────┬──────────────┬──────────────┬───────────┤
|
||||
│ 총 채권 │ 추심 진행 │ 이달(?) │ 미회수(?) │
|
||||
│ 3.5억원 │30,123,000원 │ 3,123,000원 │ 2.8억원 │
|
||||
└──────────────┴──────────────┴──────────────┴───────────┘
|
||||
│ ℹ️ (주)대한전자 건 지급명령 신청 완료. 법원 결정까지... │
|
||||
│ ⚠️ (주)삼성테크 건 회수 불가 판정. 대손 처리 검토... │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| 요소 | 설명 | 클릭 동작 |
|
||||
|------|------|----------|
|
||||
| 채권추심 현황 확록 | 채권 추심 목록 | 미상대금 수심관리 화면으로 이동 |
|
||||
|
||||
---
|
||||
|
||||
### 섹션 10: 부가세 현황 (Page 34~35)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 🔴 부가세 현황 │
|
||||
├──────────────┬──────────────┬──────────────┬───────────┤
|
||||
│ 예상 납부세액 │ 예상 납부세액 │ 금액 │ 건수 │
|
||||
│ 30.5억원 │ 20.5억원 │ 1.1억원 │ 3건 │
|
||||
└──────────────┴──────────────┴──────────────┴───────────┘
|
||||
│ ⚠️ 2026년 1기 예정신고 기한, 예상 환급세액은 5,200... │
|
||||
│ ⚠️ 2026년 1기 예정신고 기한, 예상 납부세액은 118,100... │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| 요소 | 설명 | 클릭 동작 |
|
||||
|------|------|----------|
|
||||
| 부가세 현황 확록 | 납부세액 내역 | 해당 납부세액 내역 상세 팝업 표시 |
|
||||
|
||||
---
|
||||
|
||||
### 섹션 11: 캘린더 (Page 34~35)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ < 2026년 1월 > [일정추가] [일우 월일요] │
|
||||
│ [전체▼] [발주▼] [사업▼] │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ 일 월 화 수 목 금 토 │
|
||||
│ 1 2 3 4 5 │
|
||||
│ 6 7 8 9 10 11 12 ← 6일 선택 (주황색) │
|
||||
│ 13 14 15 16 17 18 19 토/일 배경 노란색 │
|
||||
│ 20 21 22 23 24 25 26 │
|
||||
│ 27 28 29 30 31 │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ 1월 6일 화요일 총 4건 │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ ● 제목: 부서세 ✏️ │
|
||||
│ 기간: 2026-01-01~01-06 │
|
||||
│ 시간: 09:00 ~ 12:00 │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ ● 제목: 회의 │
|
||||
│ 기간: 2026-01-01~01-07 │
|
||||
│ 시간: 전일 │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ ● 제목: 1,123 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| 요소 | 설명 | 클릭 동작 |
|
||||
|------|------|----------|
|
||||
| 일정추가 버튼 | 일정 추가 | (미정) |
|
||||
| 일우 월일요 버튼 | 일정/다음달 스케쥴 표시 | 일정/다음달 스케쥴 토글 |
|
||||
| 필터 셀렉트 | 전체, 발주, 사업 등 | 일정 유형 필터링 (다중선택) |
|
||||
| 날짜 클릭 | 해당 날짜 선택 | 선택 날짜 일정 목록 표시 |
|
||||
| 일정 항목 | 개별 일정 | 일정 상세 팝업 표시 |
|
||||
| 수정 아이콘 (✏️) | 일정 수정 | 일정 수정 화면으로 이동 |
|
||||
|
||||
**달력 스타일:**
|
||||
- 토요일/일요일: 배경 노란색
|
||||
- 선택된 날짜: 배경 주황색
|
||||
- 이전/다음 달: 이전달/다음달 이동
|
||||
|
||||
---
|
||||
|
||||
## 체크리스트
|
||||
|
||||
### 1. 사전 준비 ✅
|
||||
- [x] 기존 Dashboard 컴포넌트 백업 (`Dashboard.tsx.backup2`)
|
||||
- [x] 기존 MainDashboard 컴포넌트 백업 (`MainDashboard.tsx.backup`)
|
||||
- [x] CEO Dashboard 디렉토리 구조 생성
|
||||
|
||||
### 2. 컴포넌트 구조 생성
|
||||
```
|
||||
src/components/business/CEODashboard/
|
||||
├── index.tsx # 메인 컴포넌트 (export)
|
||||
├── CEODashboard.tsx # 메인 레이아웃
|
||||
├── types.ts # 타입 정의
|
||||
├── actions.ts # Server Actions
|
||||
├── sections/
|
||||
│ ├── DashboardHeader.tsx # 헤더 (항목 설정 버튼)
|
||||
│ ├── TodayIssueSection.tsx # 오늘의 이슈
|
||||
│ ├── DailyReportSection.tsx # 일일 일보
|
||||
│ ├── MonthlyExpenseSection.tsx # 당월 예상 지출 내역
|
||||
│ ├── CardManagementSection.tsx # 카드/가지급금 관리
|
||||
│ ├── EntertainmentSection.tsx # 접대비 현황
|
||||
│ ├── WelfareSection.tsx # 복리후생비 현황
|
||||
│ ├── ReceivableSection.tsx # 미수금 현황
|
||||
│ ├── DebtCollectionSection.tsx # 채권추심 현황
|
||||
│ ├── VatSection.tsx # 부가세 현황
|
||||
│ └── CalendarSection.tsx # 캘린더
|
||||
└── dialogs/ # Phase 2에서 구현
|
||||
├── ItemSettingDialog.tsx # 항목 설정 팝업
|
||||
├── DailyReportDialog.tsx # 일일 일보 정보 팝업
|
||||
└── ...
|
||||
```
|
||||
|
||||
### 3. 섹션별 구현 체크리스트 ✅
|
||||
|
||||
#### 3.1 대시보드 헤더 ✅
|
||||
- [x] 로고 영역
|
||||
- [x] 제목 + 설명
|
||||
- [x] 항목 설정 버튼
|
||||
- [ ] 항목 설정 팝업 연동 (Phase 2)
|
||||
|
||||
#### 3.2 오늘의 이슈 ✅
|
||||
- [x] 8개 이슈 카드 그리드 (4x2)
|
||||
- [x] 각 카드 클릭 시 해당 화면 이동
|
||||
- [x] 반전 재고 빨간색 강조
|
||||
- [x] 제규 신고 D-day 표시
|
||||
|
||||
#### 3.3 일일 일보 ✅
|
||||
- [x] 날짜 표시 (년/월/일/요일)
|
||||
- [x] 4개 지표 카드
|
||||
- [x] 체크포인트 메시지 (경고/정보)
|
||||
- [ ] 클릭 시 일일 일보 팝업 (Phase 2)
|
||||
|
||||
#### 3.4 당월 예상 지출 내역 ✅
|
||||
- [x] 4개 금액 카드
|
||||
- [x] 전월 대비 증감 표시
|
||||
- [x] 체크포인트 메시지
|
||||
- [ ] 클릭 시 상세 팝업 (Phase 2)
|
||||
|
||||
#### 3.5 카드/가지급금 관리 ✅
|
||||
- [x] 4개 금액 카드
|
||||
- [x] 체크포인트 메시지
|
||||
- [x] 클릭 시 해당 화면 이동
|
||||
|
||||
#### 3.6 접대비 현황 ✅
|
||||
- [x] 4개 금액 카드
|
||||
- [x] 체크포인트 메시지
|
||||
- [ ] 클릭 시 상세 팝업 (Phase 2)
|
||||
|
||||
#### 3.7 복리후생비 현황 ✅
|
||||
- [x] 4개 금액 카드
|
||||
- [x] 체크포인트 메시지
|
||||
|
||||
#### 3.8 미수금 현황 ✅
|
||||
- [x] 4개 금액 카드 (기간별 분류)
|
||||
- [x] 매출/입금 서브 정보
|
||||
- [x] 체크포인트 메시지
|
||||
- [x] 클릭 시 미수금 상세 화면 이동
|
||||
|
||||
#### 3.9 채권추심 현황 ✅
|
||||
- [x] 4개 금액 카드
|
||||
- [x] 체크포인트 메시지
|
||||
- [x] 클릭 시 미상대금 수심관리 화면 이동
|
||||
|
||||
#### 3.10 부가세 현황 ✅
|
||||
- [x] 4개 금액 카드
|
||||
- [x] 체크포인트 메시지
|
||||
- [ ] 클릭 시 납부세액 내역 팝업 (Phase 2)
|
||||
|
||||
#### 3.11 캘린더 ✅
|
||||
- [x] ScheduleCalendar 공통 컴포넌트 활용
|
||||
- [x] 일정추가 버튼
|
||||
- [x] 필터 셀렉트 (전체/발주/사업/회의/세금)
|
||||
- [ ] 토/일 배경 노란색 스타일 커스터마이징 (추후)
|
||||
- [ ] 선택 날짜 주황색 스타일 (추후)
|
||||
- [x] 선택 날짜 일정 목록 표시
|
||||
- [ ] 일정 항목 클릭 시 상세 팝업 (Phase 2)
|
||||
- [x] 수정 아이콘 클릭 시 수정 화면 이동
|
||||
|
||||
### 4. 대시보드 교체 ✅
|
||||
- [x] Dashboard.tsx에서 MainDashboard → CEODashboard로 교체
|
||||
- [x] 타입 체크 통과
|
||||
|
||||
---
|
||||
|
||||
## 연동 페이지 목록 (오늘의 이슈 클릭 시)
|
||||
|
||||
| 이슈 항목 | 연동 페이지 | 경로 (예상) |
|
||||
|----------|------------|------------|
|
||||
| 수주 | 수주 관리 | `/sales/orders` |
|
||||
| 채권 추심 | 채권 추심 관리 | `/accounting/debt-collection` |
|
||||
| 반전 재고 | 재고 관리 | `/inventory/stock` |
|
||||
| 제규 신고 | 세무 관리 | `/accounting/tax` |
|
||||
| 신규 업체 등록 | 업체 관리 | `/partners/vendors` |
|
||||
| 연차 | 연차 관리 | `/hr/vacation` |
|
||||
| 발주 | 발주 관리 | `/purchase/orders` |
|
||||
| 결재 요청 | 결재 관리 | `/approval/pending` |
|
||||
|
||||
---
|
||||
|
||||
## 팝업 목록 (Phase 2에서 구현)
|
||||
|
||||
| 팝업 이름 | 트리거 | 내용 |
|
||||
|----------|--------|------|
|
||||
| 항목 설정 팝업 | 항목 설정 버튼 클릭 | 대시보드 표시 항목 설정 |
|
||||
| 일일 일보 정보 팝업 | 일일 일보 영역 클릭 | 일일 일보 상세 정보 |
|
||||
| 해당월 예상 지출 상세 팝업 | 당월 예상 지출 클릭 | 지출 상세 내역 |
|
||||
| 납부세액 내역 상세 팝업 | 부가세 현황 클릭 | 납부세액 상세 내역 |
|
||||
| 일정 상세 팝업 | 일정 항목 클릭 | 일정 상세 정보 |
|
||||
|
||||
---
|
||||
|
||||
## 참고 사항
|
||||
|
||||
### 기존 컴포넌트 재활용
|
||||
- `ComprehensiveAnalysis`: 많은 섹션 패턴 참고 가능
|
||||
- `SectionTitle`: 섹션 제목 컴포넌트
|
||||
- `AmountCardItem`: 금액 카드 컴포넌트
|
||||
- `CheckPointItem`: 체크포인트 메시지 컴포넌트
|
||||
- `ScheduleCalendar`: 달력 공통 컴포넌트
|
||||
- 월/주 뷰 지원
|
||||
- 이벤트/뱃지 표시
|
||||
- 커스터마이징 가능
|
||||
|
||||
### 스타일 가이드
|
||||
- 빨간색 강조: 위험/긴급 항목 (반전 재고 등)
|
||||
- 주황색: 선택된 날짜
|
||||
- 노란색 배경: 토요일/일요일
|
||||
- 체크포인트 아이콘:
|
||||
- ✅ 성공 (초록)
|
||||
- ⚠️ 경고 (주황)
|
||||
- ❌ 에러 (빨강)
|
||||
- ℹ️ 정보 (파랑)
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
|
||||
| 날짜 | 작업 내용 | 상태 |
|
||||
|------|----------|------|
|
||||
| 2026-01-07 | 계획서 작성 | 완료 |
|
||||
| 2026-01-07 | Phase 1 본 화면 구현 완료 (11개 섹션) | 완료 |
|
||||
@@ -0,0 +1,130 @@
|
||||
# 대시보드 항목 설정 팝업 구현 계획서
|
||||
|
||||
## 개요
|
||||
- **화면명**: 항목 설정_대시보드 팝업
|
||||
- **목적**: CEO 대시보드에 표시할 섹션들을 사용자가 ON/OFF로 선택할 수 있는 설정 팝업
|
||||
- **경로**: 대시보드 > 항목 설정 버튼 클릭 시 팝업 표시
|
||||
|
||||
## 기능 요구사항
|
||||
|
||||
### 1. 기본 구조
|
||||
- 모달/다이얼로그 형태의 팝업
|
||||
- 헤더: "항목 설정" 제목 + X 닫기 버튼
|
||||
- 푸터: 취소 | 저장 버튼
|
||||
|
||||
### 2. 섹션별 ON/OFF 토글
|
||||
|
||||
#### 오늘의 이슈 (전체 토글 + 개별 토글)
|
||||
| 항목 | 기본값 | 비고 |
|
||||
|------|--------|------|
|
||||
| 오늘의 이슈 (전체) | ON | 빨간 배경 - 전체 ON/OFF |
|
||||
| 수주 | ON | |
|
||||
| 채권 추심 | ON | |
|
||||
| 안전 재고 | ON | |
|
||||
| 세금 신고 | OFF | |
|
||||
| 신규 업체 등록 | OFF | |
|
||||
| 연차 | ON | |
|
||||
| 지각 | ON | |
|
||||
| 결근 | OFF | |
|
||||
| 발주 | OFF | |
|
||||
| 결재 요청 | OFF | |
|
||||
|
||||
#### 메인 섹션 토글 (접기/펼치기 가능)
|
||||
| 섹션 | 기본값 | 하위 설정 |
|
||||
|------|--------|----------|
|
||||
| 일일 일보 | ON | - |
|
||||
| 당월 예상 지출 내역 | ON | - |
|
||||
| 카드/가지급금 관리 | ON | - |
|
||||
| 접대비 현황 | ON | 접대비 한도 관리 (연간/분기), 기업 구분 |
|
||||
| 복리후생비 현황 | ON | 복리후생비 한도 관리, 계산 방식, 금액 설정 |
|
||||
| 미수금 현황 | ON | 미수금 상위 회사 현황 |
|
||||
| 채권추심 현황 | ON | - |
|
||||
| 부가세 현황 | ON | - |
|
||||
| 캘린더 | ON | - |
|
||||
|
||||
### 3. 상세 설정 옵션
|
||||
|
||||
#### 접대비 현황 하위 설정
|
||||
- 접대비 한도 관리: 연간 / 분기 선택 (드롭다운)
|
||||
- 기업 구분: 기업 선택 (드롭다운) + 설명 버튼
|
||||
|
||||
#### 복리후생비 현황 하위 설정
|
||||
- 복리후생비 한도 관리: 연간 / 분기 선택 (드롭다운)
|
||||
- 계산 방식: 직원당 정해 금액 방식 / 연봉 총액 X 비율 방식 (드롭다운)
|
||||
- 직원당 정해 금액/월: 금액 입력 (계산 방식이 "직원당 정해 금액 방식"일 때)
|
||||
- 비율: % 입력 (계산 방식이 "연봉 총액 X 비율 방식"일 때)
|
||||
- 연간 복리후생비총액: 자동 계산 또는 직접 입력
|
||||
|
||||
### 4. 기업 구분 설명 패널
|
||||
- 1-2 버튼 클릭 시 기업 구분 기준 설명 펼침/접힘
|
||||
- 중소기업 판단 기준 설명 (자본총액 기준, 매출액 기준)
|
||||
- 정보 제공용 (읽기 전용)
|
||||
|
||||
### 5. 데이터 저장
|
||||
- localStorage 또는 API를 통한 설정 저장
|
||||
- 저장 버튼 클릭 시 설정 적용 및 대시보드 새로고침
|
||||
- 취소 버튼 클릭 시 변경사항 무시하고 팝업 닫기
|
||||
|
||||
---
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
### Phase 1: 기본 팝업 구조
|
||||
- [x] 1.1 DashboardSettingsDialog 컴포넌트 생성
|
||||
- [x] 1.2 타입 정의 (DashboardSettings 인터페이스)
|
||||
- [x] 1.3 기본 다이얼로그 UI 구현 (헤더, 푸터)
|
||||
- [x] 1.4 CEODashboard에서 팝업 연결
|
||||
|
||||
### Phase 2: 오늘의 이슈 섹션
|
||||
- [x] 2.1 전체 토글 (빨간 배경) 구현
|
||||
- [x] 2.2 개별 항목 토글 목록 구현
|
||||
- [x] 2.3 전체 토글 연동 (전체 OFF 시 개별 모두 OFF)
|
||||
|
||||
### Phase 3: 메인 섹션 토글
|
||||
- [x] 3.1 접기/펼치기 가능한 섹션 아코디언 구현
|
||||
- [x] 3.2 일일 일보 ~ 캘린더 섹션 토글 구현
|
||||
- [x] 3.3 섹션별 ON/OFF 상태 관리
|
||||
|
||||
### Phase 4: 상세 설정 옵션
|
||||
- [x] 4.1 접대비 현황 하위 설정 (한도 관리, 기업 구분)
|
||||
- [x] 4.2 복리후생비 현황 하위 설정 (한도 관리, 계산 방식, 금액)
|
||||
- [ ] 4.3 기업 구분 설명 패널 (펼침/접힘) - 기획서 확인 후 추가 구현 필요
|
||||
|
||||
### Phase 5: 데이터 연동
|
||||
- [x] 5.1 설정 상태 관리 (useState/useReducer)
|
||||
- [x] 5.2 localStorage 저장/불러오기
|
||||
- [x] 5.3 대시보드에 설정 적용 (조건부 렌더링)
|
||||
|
||||
### Phase 6: 마무리
|
||||
- [x] 6.1 스타일 정리 및 반응형 대응
|
||||
- [ ] 6.2 테스트 및 검증 (빌드 확인 필요)
|
||||
|
||||
---
|
||||
|
||||
## 파일 구조
|
||||
|
||||
```
|
||||
src/components/business/CEODashboard/
|
||||
├── CEODashboard.tsx (수정 완료)
|
||||
├── components.tsx
|
||||
├── types.ts (수정 완료 - 설정 타입 추가)
|
||||
├── dialogs/
|
||||
│ └── DashboardSettingsDialog.tsx (신규 생성 완료)
|
||||
├── hooks/
|
||||
│ └── useDashboardSettings.ts (필요 시 추가)
|
||||
└── sections/
|
||||
└── ... (기존)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 참고사항
|
||||
- 기획서 Description 영역의 번호(01, 02 등)는 설명용이므로 UI에 구현하지 않음
|
||||
- 디자인은 프로젝트 기존 Dialog/Switch 컴포넌트 패턴 따름
|
||||
|
||||
## 구현 완료 (2026-01-08)
|
||||
- DashboardSettingsDialog 컴포넌트 생성
|
||||
- 커스텀 ToggleSwitch 컴포넌트 (ON/OFF 라벨, 색상 지원)
|
||||
- Collapsible 기반 아코디언 섹션 구현
|
||||
- localStorage 기반 설정 영속화
|
||||
- 대시보드 섹션 조건부 렌더링 적용
|
||||
122
claudedocs/dashboard/[IMPL-2026-02-11] favorites-feature.md
Normal file
122
claudedocs/dashboard/[IMPL-2026-02-11] favorites-feature.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# 즐겨찾기(Favorites) 기능 구현
|
||||
|
||||
> 2026-02-11 | 상태: 완료 (localStorage 기반) | 추후: API 전환 예정
|
||||
|
||||
## 개요
|
||||
|
||||
사이드바 메뉴에 별표(즐겨찾기) 기능을 추가하여 사용자가 자주 쓰는 메뉴를 헤더에 동적으로 표시.
|
||||
기존 하드코딩된 종합분석/품질인정심사 버튼을 제거하고 사용자 선택 기반으로 전환.
|
||||
|
||||
## 파일 구조
|
||||
|
||||
| 파일 | 작업 | 설명 |
|
||||
|------|------|------|
|
||||
| `src/stores/favoritesStore.ts` | NEW | Zustand persist 스토어 |
|
||||
| `src/lib/utils/menuTransform.ts` | MODIFY | reverseIconMap, getIconName, DEFAULT_FAVORITES 추가 |
|
||||
| `src/components/layout/HeaderFavoritesBar.tsx` | NEW | 헤더 즐겨찾기 바 (반응형) |
|
||||
| `src/components/layout/Sidebar.tsx` | MODIFY | leaf 메뉴에 별표 토글 추가 |
|
||||
| `src/layouts/AuthenticatedLayout.tsx` | MODIFY | 하드코딩 버튼 → HeaderFavoritesBar 교체 |
|
||||
|
||||
## 핵심 동작
|
||||
|
||||
### 즐겨찾기 등록/해제
|
||||
- 사이드바 leaf 메뉴(children 없는 항목) hover 시 별표 아이콘 표시
|
||||
- 이미 즐겨찾기인 항목은 항상 노란 별표 표시
|
||||
- 별표 클릭으로 토글 (메뉴 클릭과 분리 - `e.stopPropagation()`)
|
||||
- sidebar collapsed 상태에서는 별표 숨김
|
||||
|
||||
### 헤더 표시 (반응형)
|
||||
| 조건 | 표시 방식 |
|
||||
|------|----------|
|
||||
| 데스크톱 (1024px+), 1~8개 | 아이콘 버튼 + Tooltip |
|
||||
| 데스크톱 (1024px+), 9~10개 | ★ 드롭다운 |
|
||||
| 태블릿 (768~1024px) | ★ 드롭다운 |
|
||||
| 모바일 (<768px) | ★ 드롭다운 |
|
||||
|
||||
### 제한
|
||||
- 최대 **10개**
|
||||
- 초과 시 토스트: `즐겨찾기는 최대 10개까지 등록할 수 있습니다.`
|
||||
|
||||
### 저장
|
||||
- **현재**: localStorage (`sam-favorites-{userId}` 키)
|
||||
- Zustand persist 사용, 사용자별 분리 저장
|
||||
- 기본값 없음 (사용자가 직접 추가한 것만 표시)
|
||||
|
||||
## 주요 구현 상세
|
||||
|
||||
### favoritesStore.ts
|
||||
```typescript
|
||||
interface FavoriteItem {
|
||||
id: string; // 메뉴 id
|
||||
label: string; // 메뉴 라벨
|
||||
iconName: string; // 아이콘 문자열 (iconMap 키)
|
||||
path: string; // 라우트 경로
|
||||
addedAt: number; // 추가 시각 (정렬용)
|
||||
}
|
||||
|
||||
// toggleFavorite 반환값으로 토스트 제어
|
||||
type ToggleResult = 'added' | 'removed' | 'max_reached';
|
||||
```
|
||||
|
||||
### menuTransform.ts 확장
|
||||
- `reverseIconMap`: iconMap을 뒤집어 `LucideIcon → string` 조회
|
||||
- `getIconName(icon)`: 아이콘 컴포넌트 → 문자열 이름 변환
|
||||
- `DEFAULT_FAVORITES`: 종합분석 + 품질인정심사 (현재 미사용, 참고용 보관)
|
||||
|
||||
### button 중첩 방지
|
||||
별표 아이콘은 메뉴 `<button>` 바깥에 배치 (`<div className="group/row">` 래퍼).
|
||||
HTML 규격상 `<button>` 안에 `<button>`은 불가 → hydration 에러 유발.
|
||||
|
||||
```
|
||||
<div class="flex items-center group/row">
|
||||
<button>메뉴 항목</button> ← 메뉴 클릭
|
||||
<button>★</button> ← 별표 클릭 (형제 관계)
|
||||
</div>
|
||||
```
|
||||
|
||||
## API 전환 계획
|
||||
|
||||
현재 localStorage 기반이라 기기/브라우저별로 동기화되지 않음.
|
||||
추후 API 준비되면 스토어 내부만 교체하면 됨.
|
||||
|
||||
### 변경 범위
|
||||
- `favoritesStore.ts`만 수정 (컴포넌트 수정 불필요)
|
||||
|
||||
### 예상 API 엔드포인트
|
||||
```
|
||||
GET /api/v1/favorites → 목록 조회
|
||||
POST /api/v1/favorites → 추가
|
||||
DELETE /api/v1/favorites/{menuId} → 삭제
|
||||
```
|
||||
|
||||
### 전환 방식
|
||||
```typescript
|
||||
// Before (localStorage)
|
||||
toggleFavorite: (item) => {
|
||||
// Zustand set()으로 localStorage 저장
|
||||
}
|
||||
|
||||
// After (API)
|
||||
toggleFavorite: async (item) => {
|
||||
const exists = get().favorites.some(f => f.id === item.id);
|
||||
if (exists) {
|
||||
await fetch(`/api/v1/favorites/${item.id}`, { method: 'DELETE' });
|
||||
} else {
|
||||
await fetch('/api/v1/favorites', { method: 'POST', body: JSON.stringify(item) });
|
||||
}
|
||||
// 성공 후 로컬 상태 업데이트
|
||||
}
|
||||
```
|
||||
|
||||
### 초기 로딩
|
||||
```typescript
|
||||
// AuthenticatedLayout useEffect에서
|
||||
const res = await fetch('/api/v1/favorites');
|
||||
const data = await res.json();
|
||||
useFavoritesStore.getState().setFavorites(data);
|
||||
```
|
||||
|
||||
### 주의사항
|
||||
- API 실패 시 localStorage fallback 고려
|
||||
- 낙관적 업데이트(optimistic update)로 UX 저하 방지
|
||||
- 서버 응답 전에 UI 먼저 반영 → 실패 시 롤백
|
||||
@@ -0,0 +1,125 @@
|
||||
# CEO Dashboard 세션 컨텍스트 (2026-01-08)
|
||||
|
||||
## 세션 요약
|
||||
|
||||
### 완료된 작업
|
||||
- [x] 세금 신고 카드: "3건" → "부가세 신고 D-15" (건수 제거)
|
||||
- [x] 오늘의 이슈 카드: StatCards 스타일로 변경
|
||||
- [x] 문자열 count 스타일: `text-xl md:text-2xl font-medium` (작고 덜 굵게)
|
||||
- [x] 새로고침 버튼 제거
|
||||
- [x] 항목 설정 버튼 → 페이지 헤더 오른쪽으로 이동
|
||||
|
||||
### 수정된 파일
|
||||
- `src/components/business/CEODashboard/CEODashboard.tsx` - 데이터, 버튼 위치
|
||||
- `src/components/business/CEODashboard/components.tsx` - IssueCardItem StatCards 스타일
|
||||
- `src/components/business/CEODashboard/sections/TodayIssueSection.tsx` - 항목 설정 버튼 제거
|
||||
- `src/components/business/CEODashboard/types.ts` - icon prop 추가
|
||||
|
||||
---
|
||||
|
||||
## 다음 세션 TODO
|
||||
|
||||
### 1. 기획서 vs 구현 비교 점검
|
||||
- [ ] 기획서 스크린샷과 현재 구현 1:1 비교
|
||||
- [ ] 누락된 요소 확인
|
||||
- [ ] 임의 추가된 요소 제거
|
||||
- [ ] 빌드 확인
|
||||
|
||||
### 2. 기획서 기반 구현 정확도 개선 (우선순위 높음)
|
||||
|
||||
#### 방안 A: RULES.md 강화
|
||||
**위치**: `~/.claude/RULES.md` - "Scope Discipline & Visual Reference Fidelity" 섹션
|
||||
|
||||
**추가할 규칙**:
|
||||
```markdown
|
||||
### 기획서/스크린샷 기반 구현 프로세스
|
||||
**Priority**: 🔴 **Triggers**: 기획서, 스크린샷, PDF 제공 시
|
||||
|
||||
**필수 단계**:
|
||||
1. **요소 추출**: 스크린샷에서 모든 UI 요소 목록화
|
||||
- 버튼, 텍스트, 카드, 아이콘 등 식별
|
||||
- 위치, 스타일, 동작 기록
|
||||
2. **사용자 확인**: "이 요소들 맞아?" 확인 요청
|
||||
3. **기존 패턴 검색**: 프로젝트 내 유사 컴포넌트 찾기
|
||||
4. **구현**: 기획서 요소만 구현 (임의 추가 금지)
|
||||
5. **검증 체크리스트**: 구현 후 기획서 vs 결과 비교표 제시
|
||||
|
||||
**금지 사항**:
|
||||
- ❌ 기획서에 없는 버튼/기능 임의 추가 (예: 새로고침 버튼)
|
||||
- ❌ 기획서와 다른 위치에 요소 배치
|
||||
- ❌ "있으면 좋겠다" 기반 추가 기능
|
||||
```
|
||||
|
||||
#### 방안 B: 스킬 생성 (`/sc:implement-ui`)
|
||||
**위치**: `~/.claude/commands/sc_implement-ui.md`
|
||||
|
||||
**스킬 플로우**:
|
||||
```
|
||||
/sc:implement-ui @screenshot.png
|
||||
|
||||
1. [분석] 스크린샷에서 UI 요소 추출
|
||||
- 버튼: [목록]
|
||||
- 카드: [목록]
|
||||
- 텍스트: [목록]
|
||||
- 레이아웃: [설명]
|
||||
|
||||
2. [확인] 사용자에게 요소 목록 확인 요청
|
||||
"이 요소들이 맞나요? 누락/추가할 것 있나요?"
|
||||
|
||||
3. [패턴 검색] 기존 프로젝트에서 유사 컴포넌트 찾기
|
||||
- 검색 결과 제시
|
||||
- 재사용할 패턴 선택
|
||||
|
||||
4. [구현] 기획서 요소만 구현
|
||||
- 임의 추가 금지
|
||||
- 기존 패턴 따르기
|
||||
|
||||
5. [검증] 기획서 vs 구현 비교 체크리스트
|
||||
| 기획서 요소 | 구현 여부 | 위치 일치 | 스타일 일치 |
|
||||
|------------|----------|----------|------------|
|
||||
| 항목 설정 버튼 | ✅ | ✅ | ✅ |
|
||||
| 새로고침 버튼 | ❌ (없음) | - | - |
|
||||
```
|
||||
|
||||
**스킬 파일 예시**:
|
||||
```markdown
|
||||
# /sc:implement-ui - 기획서 기반 UI 구현
|
||||
|
||||
## 목적
|
||||
스크린샷/기획서를 정확하게 구현하기 위한 체계적 워크플로우
|
||||
|
||||
## 사용법
|
||||
/sc:implement-ui @screenshot.png
|
||||
/sc:implement-ui @design.pdf "특정 섹션 설명"
|
||||
|
||||
## 프로세스
|
||||
[위 플로우 내용]
|
||||
|
||||
## 검증 규칙
|
||||
- 기획서에 있는 것만 구현
|
||||
- 없는 것은 절대 추가하지 않음
|
||||
- 구현 후 반드시 비교 체크리스트 제시
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 문제점 분석 (이번 세션에서 발생한 이슈)
|
||||
|
||||
### 발생한 문제
|
||||
1. **새로고침 버튼**: 기획서에 없는데 임의 추가
|
||||
2. **항목 설정 버튼 위치**: 기획서와 다른 위치에 배치
|
||||
3. **세금 신고 카드**: 기획서에 건수 없는데 "3건" 추가
|
||||
|
||||
### 원인
|
||||
- 기획서 꼼꼼히 확인 안 함
|
||||
- "있으면 좋겠다" 기반 임의 추가
|
||||
- 구현 전 요소 목록화 단계 누락
|
||||
|
||||
### 해결책
|
||||
- RULES.md 강화 + 스킬 생성으로 프로세스 강제
|
||||
|
||||
---
|
||||
|
||||
## 참고 파일
|
||||
- 기획서: `/Users/byeongcheolryu/Desktop/스크린샷 2026-01-07 오후 6.55.10.png`
|
||||
- 체크리스트: `claudedocs/[IMPL-2026-01-07] ceo-dashboard-checklist.md`
|
||||
331
claudedocs/dashboard/[PLAN] ceo-dashboard-refactoring.md
Normal file
331
claudedocs/dashboard/[PLAN] ceo-dashboard-refactoring.md
Normal file
@@ -0,0 +1,331 @@
|
||||
# CEO 대시보드 리팩토링 계획
|
||||
|
||||
> 작성일: 2026-01-10
|
||||
> 대상 파일: `src/components/business/CEODashboard/`
|
||||
> 목표: 파일 분리 + 모바일(344px) 대응
|
||||
|
||||
---
|
||||
|
||||
## 1. 현재 상태 분석
|
||||
|
||||
### 1.1 파일 구조
|
||||
|
||||
```
|
||||
CEODashboard/
|
||||
├── CEODashboard.tsx # 1,648줄 ⚠️ 분리 필요
|
||||
├── components.tsx # 312줄 ✅ 적정
|
||||
├── types.ts # ~100줄 ✅ 적정
|
||||
├── sections/
|
||||
│ ├── index.ts
|
||||
│ ├── TodayIssueSection.tsx # 73줄 ✅
|
||||
│ ├── DailyReportSection.tsx # 37줄 ✅
|
||||
│ ├── MonthlyExpenseSection.tsx # 38줄 ✅
|
||||
│ ├── CardManagementSection.tsx # ~50줄 ✅
|
||||
│ ├── EntertainmentSection.tsx # ~50줄 ✅
|
||||
│ ├── WelfareSection.tsx # ~50줄 ✅
|
||||
│ ├── ReceivableSection.tsx # ~50줄 ✅
|
||||
│ ├── DebtCollectionSection.tsx # ~50줄 ✅
|
||||
│ ├── VatSection.tsx # ~50줄 ✅
|
||||
│ └── CalendarSection.tsx # ~100줄 ✅
|
||||
├── modals/
|
||||
│ ├── ScheduleDetailModal.tsx # ~200줄 ✅
|
||||
│ └── DetailModal.tsx # ~300줄 ✅
|
||||
└── dialogs/
|
||||
└── DashboardSettingsDialog.tsx # ~200줄 ✅
|
||||
```
|
||||
|
||||
### 1.2 CEODashboard.tsx 내부 분석 (1,648줄)
|
||||
|
||||
| 줄 범위 | 내용 | 줄 수 | 분리 대상 |
|
||||
|---------|------|-------|----------|
|
||||
| 1-26 | imports | 26 | - |
|
||||
| 27-370 | mockData 객체 | **344** | ✅ 분리 |
|
||||
| 371-748 | handleMonthlyExpenseCardClick (모달 config) | **378** | ✅ 분리 |
|
||||
| 749-1019 | handleCardManagementCardClick (모달 config) | **271** | ✅ 분리 |
|
||||
| 1020-1247 | handleEntertainmentCardClick (모달 config) | **228** | ✅ 분리 |
|
||||
| 1248-1375 | handleWelfareCardClick (모달 config) | **128** | ✅ 분리 |
|
||||
| 1376-1465 | handleVatClick (모달 config) | **90** | ✅ 분리 |
|
||||
| 1466-1509 | 캘린더 관련 핸들러 | 44 | - |
|
||||
| 1510-1648 | 컴포넌트 렌더링 | 139 | - |
|
||||
|
||||
**분리 대상 총합**: ~1,439줄 (87%)
|
||||
**분리 후 예상**: ~210줄
|
||||
|
||||
---
|
||||
|
||||
## 2. 분리 계획
|
||||
|
||||
### 2.1 목표 구조
|
||||
|
||||
```
|
||||
CEODashboard/
|
||||
├── CEODashboard.tsx # ~250줄 (컴포넌트 + 핸들러)
|
||||
├── components.tsx # 312줄 (유지)
|
||||
├── types.ts # ~100줄 (유지)
|
||||
├── mockData.ts # 🆕 ~350줄 (목데이터)
|
||||
├── modalConfigs/ # 🆕 모달 설정 분리
|
||||
│ ├── index.ts
|
||||
│ ├── monthlyExpenseConfigs.ts # ~380줄
|
||||
│ ├── cardManagementConfigs.ts # ~280줄
|
||||
│ ├── entertainmentConfigs.ts # ~230줄
|
||||
│ ├── welfareConfigs.ts # ~130줄
|
||||
│ └── vatConfigs.ts # ~100줄
|
||||
├── sections/ # (유지)
|
||||
├── modals/ # (유지)
|
||||
└── dialogs/ # (유지)
|
||||
```
|
||||
|
||||
### 2.2 분리 파일 상세
|
||||
|
||||
#### A. mockData.ts (신규)
|
||||
|
||||
```typescript
|
||||
// mockData.ts
|
||||
import type { CEODashboardData } from './types';
|
||||
|
||||
export const mockData: CEODashboardData = {
|
||||
todayIssue: [...],
|
||||
dailyReport: {...},
|
||||
monthlyExpense: {...},
|
||||
cardManagement: {...},
|
||||
entertainment: {...},
|
||||
welfare: {...},
|
||||
receivable: {...},
|
||||
debtCollection: {...},
|
||||
vat: {...},
|
||||
calendarSchedules: [...],
|
||||
};
|
||||
```
|
||||
|
||||
#### B. modalConfigs/index.ts (신규)
|
||||
|
||||
```typescript
|
||||
// modalConfigs/index.ts
|
||||
export { getMonthlyExpenseModalConfig } from './monthlyExpenseConfigs';
|
||||
export { getCardManagementModalConfig } from './cardManagementConfigs';
|
||||
export { getEntertainmentModalConfig } from './entertainmentConfigs';
|
||||
export { getWelfareModalConfig } from './welfareConfigs';
|
||||
export { getVatModalConfig } from './vatConfigs';
|
||||
```
|
||||
|
||||
#### C. 개별 모달 config 파일 예시
|
||||
|
||||
```typescript
|
||||
// modalConfigs/monthlyExpenseConfigs.ts
|
||||
import type { DetailModalConfig } from '../types';
|
||||
|
||||
export function getMonthlyExpenseModalConfig(cardId: string): DetailModalConfig | null {
|
||||
const configs: Record<string, DetailModalConfig> = {
|
||||
me1: { title: '당월 매입 상세', ... },
|
||||
me2: { title: '당월 카드 상세', ... },
|
||||
me3: { title: '당월 발행어음 상세', ... },
|
||||
me4: { title: '당월 지출 예상 상세', ... },
|
||||
};
|
||||
return configs[cardId] || null;
|
||||
}
|
||||
```
|
||||
|
||||
#### D. CEODashboard.tsx (리팩토링 후)
|
||||
|
||||
```typescript
|
||||
// CEODashboard.tsx (리팩토링 후 ~250줄)
|
||||
import { mockData } from './mockData';
|
||||
import {
|
||||
getMonthlyExpenseModalConfig,
|
||||
getCardManagementModalConfig,
|
||||
getEntertainmentModalConfig,
|
||||
getWelfareModalConfig,
|
||||
getVatModalConfig,
|
||||
} from './modalConfigs';
|
||||
|
||||
export function CEODashboard() {
|
||||
// 상태 관리
|
||||
const [data] = useState<CEODashboardData>(mockData);
|
||||
const [detailModalConfig, setDetailModalConfig] = useState<DetailModalConfig | null>(null);
|
||||
// ...
|
||||
|
||||
// 간소화된 핸들러
|
||||
const handleMonthlyExpenseCardClick = useCallback((cardId: string) => {
|
||||
const config = getMonthlyExpenseModalConfig(cardId);
|
||||
if (config) {
|
||||
setDetailModalConfig(config);
|
||||
setIsDetailModalOpen(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 렌더링
|
||||
return (...);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 모바일 대응 계획
|
||||
|
||||
### 3.1 적용 대상 컴포넌트
|
||||
|
||||
| 컴포넌트 | 현재 상태 | 변경 필요 |
|
||||
|----------|----------|----------|
|
||||
| TodayIssueSection | `grid-cols-2 md:grid-cols-4` | ✅ `grid-cols-1 xs:grid-cols-2 md:grid-cols-4` |
|
||||
| DailyReportSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 |
|
||||
| MonthlyExpenseSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 |
|
||||
| CardManagementSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 |
|
||||
| EntertainmentSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 |
|
||||
| WelfareSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 |
|
||||
| ReceivableSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 |
|
||||
| DebtCollectionSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 |
|
||||
| VatSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 |
|
||||
| AmountCardItem (공통) | 고정 텍스트 크기 | ✅ 반응형 텍스트 |
|
||||
| IssueCardItem (공통) | 고정 텍스트 크기 | ✅ 반응형 텍스트 |
|
||||
| PageHeader | 가로 배치 | ✅ 세로/가로 반응형 |
|
||||
|
||||
### 3.2 components.tsx 변경 사항
|
||||
|
||||
#### AmountCardItem
|
||||
|
||||
```tsx
|
||||
// Before
|
||||
<p className="text-2xl md:text-3xl font-bold">
|
||||
{formatCardAmount(card.amount)}
|
||||
</p>
|
||||
|
||||
// After
|
||||
<p className="text-lg xs:text-xl md:text-2xl lg:text-3xl font-bold truncate">
|
||||
{formatCardAmount(card.amount)}
|
||||
</p>
|
||||
<p className="text-xs xs:text-sm font-medium mb-1 xs:mb-2 break-keep">
|
||||
{card.label}
|
||||
</p>
|
||||
```
|
||||
|
||||
#### IssueCardItem
|
||||
|
||||
```tsx
|
||||
// Before
|
||||
<p className="text-2xl md:text-3xl font-bold">
|
||||
{typeof count === 'number' ? `${count}건` : count}
|
||||
</p>
|
||||
|
||||
// After
|
||||
<p className="text-lg xs:text-xl md:text-2xl lg:text-3xl font-bold">
|
||||
{typeof count === 'number' ? `${count}건` : count}
|
||||
</p>
|
||||
```
|
||||
|
||||
### 3.3 섹션 공통 변경
|
||||
|
||||
```tsx
|
||||
// Before (모든 섹션)
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
|
||||
// After
|
||||
<div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-4 gap-3 xs:gap-4">
|
||||
```
|
||||
|
||||
### 3.4 CardContent 패딩
|
||||
|
||||
```tsx
|
||||
// Before
|
||||
<CardContent className="p-6">
|
||||
|
||||
// After
|
||||
<CardContent className="p-3 xs:p-4 md:p-6">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 실행 계획
|
||||
|
||||
### Phase 1: 파일 분리 (예상 30분)
|
||||
|
||||
- [ ] **1.1** `mockData.ts` 생성 및 데이터 이동
|
||||
- [ ] **1.2** `modalConfigs/` 폴더 생성
|
||||
- [ ] **1.3** `monthlyExpenseConfigs.ts` 생성
|
||||
- [ ] **1.4** `cardManagementConfigs.ts` 생성
|
||||
- [ ] **1.5** `entertainmentConfigs.ts` 생성
|
||||
- [ ] **1.6** `welfareConfigs.ts` 생성
|
||||
- [ ] **1.7** `vatConfigs.ts` 생성
|
||||
- [ ] **1.8** `modalConfigs/index.ts` 생성
|
||||
- [ ] **1.9** `CEODashboard.tsx` 리팩토링
|
||||
- [ ] **1.10** import 정리 및 동작 확인
|
||||
|
||||
### Phase 2: 모바일 대응 (예상 30분)
|
||||
|
||||
- [ ] **2.1** `components.tsx` - AmountCardItem 반응형 적용
|
||||
- [ ] **2.2** `components.tsx` - IssueCardItem 반응형 적용
|
||||
- [ ] **2.3** `sections/*.tsx` - 그리드 반응형 적용 (일괄)
|
||||
- [ ] **2.4** `sections/*.tsx` - CardContent 패딩 반응형 적용
|
||||
- [ ] **2.5** PageHeader 반응형 확인
|
||||
- [ ] **2.6** 344px 테스트 및 미세 조정
|
||||
|
||||
### Phase 3: 검증 (예상 15분)
|
||||
|
||||
- [ ] **3.1** 빌드 확인 요청
|
||||
- [ ] **3.2** 데스크탑(1280px) 동작 확인
|
||||
- [ ] **3.3** 태블릿(768px) 동작 확인
|
||||
- [ ] **3.4** 모바일(375px) 동작 확인
|
||||
- [ ] **3.5** Galaxy Fold(344px) 동작 확인
|
||||
|
||||
---
|
||||
|
||||
## 5. 예상 결과
|
||||
|
||||
### 5.1 파일 크기 변화
|
||||
|
||||
| 파일 | Before | After |
|
||||
|------|--------|-------|
|
||||
| CEODashboard.tsx | 1,648줄 | ~250줄 |
|
||||
| mockData.ts | - | ~350줄 |
|
||||
| modalConfigs/*.ts | - | ~1,100줄 (5개 파일) |
|
||||
|
||||
### 5.2 장점
|
||||
|
||||
1. **유지보수성**: 각 파일이 단일 책임 원칙 준수
|
||||
2. **재사용성**: 모달 config를 다른 곳에서 재사용 가능
|
||||
3. **확장성**: 새 모달 추가 시 별도 파일로 분리
|
||||
4. **가독성**: 핵심 로직만 CEODashboard.tsx에 유지
|
||||
5. **API 전환 용이**: mockData.ts만 교체하면 됨
|
||||
|
||||
### 5.3 모바일 개선 효과
|
||||
|
||||
| 항목 | Before (344px) | After (344px) |
|
||||
|------|----------------|---------------|
|
||||
| 카드 배치 | 2열 (160px/카드) | 1열 (320px/카드) |
|
||||
| 금액 표시 | 잘림 가능 | 완전 표시 |
|
||||
| 라벨 표시 | 잘림 가능 | 줄바꿈/truncate |
|
||||
| 패딩 | 과다 (24px) | 적정 (12px) |
|
||||
|
||||
---
|
||||
|
||||
## 6. 참고 문서
|
||||
|
||||
- **모바일 대응 가이드**: `claudedocs/guides/[GUIDE] mobile-responsive-patterns.md`
|
||||
- **기존 테스트 계획**: `claudedocs/[PLAN] mobile-overflow-testing.md`
|
||||
|
||||
---
|
||||
|
||||
## 7. 의사결정 사항
|
||||
|
||||
### Q1: mockData를 별도 파일로?
|
||||
- **결정**: ✅ 분리
|
||||
- **이유**: 향후 API 연동 시 교체 용이
|
||||
|
||||
### Q2: 모달 config를 폴더로?
|
||||
- **결정**: ✅ 폴더로 분리
|
||||
- **이유**: 각 config가 100줄 이상, 단일 파일은 여전히 큼
|
||||
|
||||
### Q3: 모바일에서 1열 vs 2열?
|
||||
- **결정**: 344px 이하 1열, 375px 이상 2열
|
||||
- **이유**: Galaxy Fold 160px 카드는 너무 좁음
|
||||
|
||||
---
|
||||
|
||||
## 8. 시작 조건
|
||||
|
||||
- [x] 계획서 작성 완료
|
||||
- [x] 모바일 가이드 작성 완료
|
||||
- [ ] 사용자 승인
|
||||
|
||||
---
|
||||
|
||||
> **다음 단계**: 계획 승인 후 Phase 1 (파일 분리) 시작
|
||||
150
claudedocs/dev/[FIX-2026-01-29] typecheck-errors-checklist.md
Normal file
150
claudedocs/dev/[FIX-2026-01-29] typecheck-errors-checklist.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# TypeScript 타입에러 전체 수정 체크리스트
|
||||
|
||||
- **날짜**: 2026-01-29
|
||||
- **총 에러**: 408개 / 155파일
|
||||
- **목표**: 전체 `tsc --noEmit` 에러 0
|
||||
- **결과**: ✅ **0 errors** (408 → 0 완료)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 공통 템플릿 (연쇄 에러 감소 기대)
|
||||
|
||||
- [x] `components/templates/UniversalListPage` (6 errors) ✅
|
||||
- [x] `components/templates/IntegratedDetailTemplate` (5 errors) ✅
|
||||
- [x] `components/templates` (기타 2 errors) ✅
|
||||
> Phase 1 완료: 408 → 364 (44개 감소, 연쇄 해소 포함)
|
||||
|
||||
## Phase 2: 페이지 (app/[locale]) — 80 errors
|
||||
|
||||
- [x] `app/[locale]` 페이지 타입 에러 수정 (80 errors) ✅
|
||||
|
||||
## Phase 3: 재고/자재 — 36 errors
|
||||
|
||||
- [x] `components/material/StockStatus` (31 errors) ✅
|
||||
- [x] `components/material/ReceivingManagement` (5 errors) ✅
|
||||
|
||||
## Phase 4: 생산 — 30 errors
|
||||
|
||||
- [x] `components/production/WorkOrders` (18 errors) ✅
|
||||
- [x] `components/production/WorkerScreen` (6 errors) ✅
|
||||
- [x] `components/production/WorkResults` (6 errors) ✅
|
||||
|
||||
## Phase 5: 주문/출고 — 36 errors
|
||||
|
||||
- [x] `components/outbound/ShipmentManagement` (18 errors) ✅
|
||||
- [x] `components/orders` (18 errors) ✅
|
||||
- [x] `components/orders/documents` (4 errors) ✅
|
||||
|
||||
## Phase 6: 견적/단가 — 18 errors
|
||||
|
||||
- [x] `components/quotes` (16 errors) ✅
|
||||
- [x] `components/pricing` (2 errors) ✅
|
||||
|
||||
## Phase 7: 건설 — 50 errors
|
||||
|
||||
- [x] `components/business/construction/item-management` (8 errors) ✅
|
||||
- [x] `components/business/construction/order-management` (7 errors) ✅
|
||||
- [x] `components/business/construction/structure-review` (5 errors) ✅
|
||||
- [x] `components/business/construction/site-management` (5 errors) ✅
|
||||
- [x] `components/business/construction/estimates` (4 errors) ✅
|
||||
- [x] `components/business/construction/estimates/sections` (3 errors) ✅
|
||||
- [x] `components/business/construction/pricing-management` (3 errors) ✅
|
||||
- [x] `components/business/construction/handover-report` (3 errors) ✅
|
||||
- [x] `components/business/construction/worker-status` (2 errors) ✅
|
||||
- [x] `components/business/construction/management` (2 errors) ✅
|
||||
- [x] `components/business/construction/contract` (2 errors) ✅
|
||||
- [x] `components/business/construction/utility-management` (1 error) ✅
|
||||
- [x] `components/business/construction/site-briefings` (1 error) ✅
|
||||
- [x] `components/business/construction/progress-billing` (1 error) ✅
|
||||
- [x] `components/business/construction/order-management/dialogs` (1 error) ✅
|
||||
- [x] `components/business/construction/category-management` (1 error) ✅
|
||||
- [x] `components/business/construction/bidding` (1 error) ✅
|
||||
|
||||
## Phase 8: HR — 24 errors
|
||||
|
||||
- [x] `components/hr/CardManagement/_legacy` (13 errors) ✅
|
||||
- [x] `components/hr/CardManagement` (3 errors) ✅
|
||||
- [x] `components/hr/VacationManagement` (2 errors) ✅
|
||||
- [x] `components/hr/EmployeeManagement` (2 errors) ✅
|
||||
- [x] `components/hr/SalaryManagement` (1 error) ✅
|
||||
- [x] `components/attendance` (2 errors) ✅
|
||||
|
||||
## Phase 9: 설정 — 26 errors
|
||||
|
||||
- [x] `components/settings/PermissionManagement` (10 errors) ✅
|
||||
- [x] `components/settings/AccountManagement/_legacy` (6 errors) ✅
|
||||
- [x] `components/settings/PopupManagement` (4 errors) ✅
|
||||
- [x] `components/settings/SubscriptionManagement` (3 errors) ✅
|
||||
- [x] `components/settings/AccountManagement` (2 errors) ✅
|
||||
- [x] `components/settings/NotificationSettings` (2 errors) ✅
|
||||
- [x] `components/settings/CompanyInfoManagement` (2 errors) ✅
|
||||
- [x] `components/settings/TitleManagement` (1 error) ✅
|
||||
- [x] `components/settings/RankManagement` (1 error) ✅
|
||||
|
||||
## Phase 10: 게시판 — 15 errors
|
||||
|
||||
- [x] `components/board/BoardManagement` (9 errors) ✅
|
||||
- [x] `components/board/BoardList` (3 errors) ✅
|
||||
- [x] `components/board/CommentSection` (1 error) ✅
|
||||
- [x] `components/board/BoardForm` (1 error) ✅
|
||||
- [x] `components/board/BoardDetail` (1 error) ✅
|
||||
|
||||
## Phase 11: 회계 — 17 errors
|
||||
|
||||
- [x] `components/accounting/VendorManagement` (4 errors) ✅
|
||||
- [x] `components/accounting/BadDebtCollection` (4 errors) ✅
|
||||
- [x] `components/accounting/CardTransactionInquiry` (3 errors) ✅
|
||||
- [x] `components/accounting/WithdrawalManagement` (2 errors) ✅
|
||||
- [x] `components/accounting/DepositManagement` (2 errors) ✅
|
||||
- [x] `components/accounting/BillManagement` (2 errors) ✅
|
||||
- [x] `components/accounting/VendorLedger` (1 error) ✅
|
||||
- [x] `components/accounting/SalesManagement` (1 error) ✅
|
||||
- [x] `components/accounting/PurchaseManagement` (1 error) ✅
|
||||
- [x] `components/accounting/ExpectedExpenseManagement` (1 error) ✅
|
||||
|
||||
## Phase 12: 기타 모듈 — ~30 errors
|
||||
|
||||
- [x] `components/business/CEODashboard` (3 errors) ✅
|
||||
- [x] `components/business` (기타 2 errors) ✅
|
||||
- [x] `components/clients` (4 errors) ✅
|
||||
- [x] `components/customer-center/InquiryManagement` (5 errors) ✅
|
||||
- [x] `components/customer-center/NoticeManagement` (1 error) ✅
|
||||
- [x] `components/customer-center/EventManagement` (1 error) ✅
|
||||
- [x] `components/vehicle-management` (8 errors) ✅
|
||||
- [x] `components/items` (5 errors) ✅
|
||||
- [x] `components/approval` (3 errors) ✅
|
||||
- [x] `components/dev/generators` (5 errors) ✅
|
||||
- [x] `components/quality/InspectionManagement` (1 error) ✅
|
||||
- [x] `components/process-management` (1 error) ✅
|
||||
- [x] `components/ui` (1 error) ✅
|
||||
|
||||
## Phase 13: 유틸/훅/API — 8 errors
|
||||
|
||||
- [x] `hooks` (3 errors) ✅
|
||||
- [x] `contexts` (2 errors) ✅
|
||||
- [x] `lib/api/dashboard` (2 errors) ✅
|
||||
- [x] `lib/api` (1 error) ✅
|
||||
- [x] `lib/utils` (1 error) ✅
|
||||
- [x] `app/api/pdf/generate` (1 error) ✅
|
||||
|
||||
---
|
||||
|
||||
## 진행 현황
|
||||
|
||||
| Phase | 대상 | 에러 수 | 상태 |
|
||||
|-------|------|---------|------|
|
||||
| 1 | 공통 템플릿 | 13 | ✅ 완료 |
|
||||
| 2 | 페이지 | 80 | ✅ 완료 |
|
||||
| 3 | 재고/자재 | 36 | ✅ 완료 |
|
||||
| 4 | 생산 | 30 | ✅ 완료 |
|
||||
| 5 | 주문/출고 | 36 | ✅ 완료 |
|
||||
| 6 | 견적/단가 | 18 | ✅ 완료 |
|
||||
| 7 | 건설 | 50 | ✅ 완료 |
|
||||
| 8 | HR | 24 | ✅ 완료 |
|
||||
| 9 | 설정 | 26 | ✅ 완료 |
|
||||
| 10 | 게시판 | 15 | ✅ 완료 |
|
||||
| 11 | 회계 | 17 | ✅ 완료 |
|
||||
| 12 | 기타 모듈 | ~30 | ✅ 완료 |
|
||||
| 13 | 유틸/훅/API | 8 | ✅ 완료 |
|
||||
|
||||
**최종 결과: 408 errors → 0 errors (155 files across 13 phases)**
|
||||
180
claudedocs/dev/[FIX-2026-02-09] PDF-이미지-누락-해결.md
Normal file
180
claudedocs/dev/[FIX-2026-02-09] PDF-이미지-누락-해결.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# [FIX] PDF 변환 시 이미지 누락 문제 해결
|
||||
|
||||
**날짜**: 2026-02-09
|
||||
**수정 파일**: `src/components/document-system/viewer/DocumentViewer.tsx`
|
||||
**영향 범위**: DocumentViewer를 사용하는 모든 문서 (공통 수정)
|
||||
|
||||
---
|
||||
|
||||
## 문제
|
||||
|
||||
중간검사성적서 등 문서에서 **도해 이미지**가 화면에는 정상 표시되지만, **PDF 다운로드 시 이미지가 누락**되는 현상.
|
||||
|
||||
## 원인 분석
|
||||
|
||||
### PDF 생성 흐름
|
||||
|
||||
```
|
||||
1. DocumentViewer.handlePdf()
|
||||
2. .print-area DOM 요소를 cloneWithInlineStyles()로 복제
|
||||
3. clone.outerHTML → HTML 문자열 추출
|
||||
4. /api/pdf/generate POST 전송
|
||||
5. Puppeteer가 page.setContent(html) → PDF 렌더링
|
||||
```
|
||||
|
||||
### 핵심 원인: `setContent()`의 base URL 부재
|
||||
|
||||
Puppeteer의 `page.setContent(html)`은 **로컬 HTML 문자열을 렌더링**하므로 base URL이 없다.
|
||||
|
||||
| 이미지 src 유형 | 브라우저 (화면) | Puppeteer (PDF) |
|
||||
|----------------|----------------|-----------------|
|
||||
| 상대 경로 `/uploads/img.jpg` | `localhost:3000` 기준으로 해석 | base URL 없음 → 로드 실패 |
|
||||
| 절대 URL `https://api.example.com/img.jpg` | 정상 로드 | 서버 네트워크 환경에 따라 실패 가능 |
|
||||
| Blob URL `blob:http://...` | 브라우저 메모리 참조 | Puppeteer 컨텍스트에 없음 → 로드 실패 |
|
||||
| Data URL `data:image/png;base64,...` | 정상 | 정상 (HTML에 내장) |
|
||||
|
||||
**결론**: `<img src="...">` 가 상대/절대/blob URL이면 Puppeteer PDF에서 이미지가 빠진다.
|
||||
|
||||
## 해결 방법
|
||||
|
||||
### 접근: 이미지를 base64 data URL로 인라인 변환
|
||||
|
||||
PDF API로 HTML을 보내기 **전에**, 모든 `<img>` 태그의 `src`를 **base64 data URL로 변환**하여 HTML 자체에 이미지를 내장시킨다.
|
||||
|
||||
### 구현: `convertImagesToBase64()`
|
||||
|
||||
```typescript
|
||||
// DocumentViewer.tsx에 추가
|
||||
const convertImagesToBase64 = async (original: HTMLElement, clone: HTMLElement): Promise<void> => {
|
||||
const originalImages = Array.from(original.querySelectorAll('img'));
|
||||
const clonedImages = Array.from(clone.querySelectorAll('img'));
|
||||
|
||||
await Promise.all(
|
||||
originalImages.map(async (img, index) => {
|
||||
const clonedImg = clonedImages[index];
|
||||
if (!clonedImg) return;
|
||||
|
||||
const src = img.src;
|
||||
if (!src || src.startsWith('data:')) return; // 이미 data URL이면 스킵
|
||||
|
||||
try {
|
||||
// 1차: fetch → blob → FileReader → dataURL
|
||||
const response = await fetch(src);
|
||||
const blob = await response.blob();
|
||||
const dataUrl = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
clonedImg.setAttribute('src', dataUrl);
|
||||
} catch {
|
||||
// 2차 fallback: canvas 방식
|
||||
try {
|
||||
if (img.complete && img.naturalWidth > 0) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
clonedImg.setAttribute('src', canvas.toDataURL('image/png'));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 변환 실패 시 원본 src 유지
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 호출 위치: `handlePdf()` 내부
|
||||
|
||||
```typescript
|
||||
// 변경 전
|
||||
const clonedElement = cloneWithInlineStyles(printAreaEl);
|
||||
const html = clonedElement.outerHTML;
|
||||
|
||||
// 변경 후
|
||||
const clonedElement = cloneWithInlineStyles(printAreaEl);
|
||||
await convertImagesToBase64(printAreaEl, clonedElement); // 이미지 인라인 변환
|
||||
const html = clonedElement.outerHTML;
|
||||
```
|
||||
|
||||
## 영향 범위 (공통 적용)
|
||||
|
||||
`DocumentViewer`가 모든 문서의 PDF 생성을 담당하므로, **1곳 수정으로 전체 해결**:
|
||||
|
||||
| 화면 | 이미지 필드 | 적용 |
|
||||
|------|------------|------|
|
||||
| 스크린 중간검사 | `schematicImage` (도해) | 자동 적용 |
|
||||
| 슬랫 중간검사 | `schematicImage` (도해) | 자동 적용 |
|
||||
| 조인트바 중간검사 | `schematicImage` (도해) | 자동 적용 |
|
||||
| 절곡 중간검사 | `schematicImage` (도해) | 자동 적용 |
|
||||
| 절곡 재공품 중간검사 | `schematicImage` (도해) | 자동 적용 |
|
||||
| 제품검사 성적서 (품질) | `productImages` | 자동 적용 |
|
||||
| 기타 DocumentViewer 문서 | 모든 `<img>` | 자동 적용 |
|
||||
|
||||
## 변환 전략 우선순위
|
||||
|
||||
```
|
||||
1. data URL → 스킵 (이미 인라인)
|
||||
2. fetch → blob → FileReader → dataURL (동일 출처, CORS 허용)
|
||||
3. canvas drawImage → toDataURL (로컬 이미지 fallback)
|
||||
4. 실패 시 원본 src 유지 (graceful degradation)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 미해결: 도해 이미지 데이터 파이프라인 (2026-02-09 확인)
|
||||
|
||||
### 현재 상황
|
||||
|
||||
PDF 변환(파이프 끝)은 수정 완료했으나, **데이터 주입(파이프 시작)이 연결되지 않은 상태**.
|
||||
|
||||
```
|
||||
[백엔드 설정 사이트] [sam-api] [프론트엔드]
|
||||
도해 이미지 업로드 → inspection_setting? → inspectionSetting
|
||||
검사기준 설정 (❌ 미구현) schematicImage
|
||||
↓
|
||||
DocumentViewer
|
||||
↓
|
||||
PDF (base64 변환 ✅)
|
||||
```
|
||||
|
||||
### 백엔드 API 확인 결과
|
||||
|
||||
| 항목 | 상태 |
|
||||
|------|------|
|
||||
| `ProcessStep` 테이블에 `inspection_setting` 컬럼 | ❌ 없음 |
|
||||
| `ApiProcessStep` → `inspectionSetting` 매핑 | ❌ 없음 |
|
||||
| 검사 설정 위치 | Process 레벨 `DocumentTemplate`으로 최근(2/10~11) 이동 |
|
||||
| 도해 이미지 URL 필드 | ❌ 백엔드에 미정의 |
|
||||
|
||||
### 프론트 vs 백엔드 구조 불일치
|
||||
|
||||
| 항목 | 프론트엔드 기대 | 백엔드 실제 |
|
||||
|------|----------------|-------------|
|
||||
| 위치 | `step.inspectionSetting` | `process.documentTemplate` |
|
||||
| 구조 | `{ standardName, schematicImage, appearance, dimension }` | DocumentTemplate (sections, fields) |
|
||||
| 이미지 | `schematicImage` 직접 필드 | 없음 |
|
||||
|
||||
### 프론트 누락 코드 (합치고 나서 수정 필요)
|
||||
|
||||
**`src/components/process-management/actions.ts`**:
|
||||
- `ApiProcessStep` 인터페이스에 `inspection_setting` 필드 미정의 (line 582)
|
||||
- `transformStepApiToFrontend()`에서 `inspectionSetting` 매핑 안 함 (line 599)
|
||||
|
||||
### 해결 방향
|
||||
|
||||
백엔드 머지 후 아래 중 하나로 진행:
|
||||
|
||||
| 옵션 | 내용 | 수정 위치 |
|
||||
|------|------|----------|
|
||||
| A | ProcessStep에 `inspection_setting` JSON 컬럼 추가 | 백엔드 |
|
||||
| B | API에서 DocumentTemplate → InspectionSetting 변환하여 내려주기 | 백엔드 |
|
||||
| C | 프론트에서 `process.documentTemplate` 직접 사용하도록 변경 | 프론트 |
|
||||
|
||||
**백엔드 담당자와 구조 협의 필요.**
|
||||
286
claudedocs/dev/[HOTFIX-2026-01-27] E2E-테스트-수정계획서.md
Normal file
286
claudedocs/dev/[HOTFIX-2026-01-27] E2E-테스트-수정계획서.md
Normal file
@@ -0,0 +1,286 @@
|
||||
# E2E 테스트 기반 프론트엔드 수정 계획서
|
||||
|
||||
**작성일**: 2026-01-27
|
||||
**기준**: sam-hotfix 프로젝트 1월 27일 E2E 테스트 결과
|
||||
**대상**: sam-react-prod (Next.js 프론트엔드)
|
||||
|
||||
---
|
||||
|
||||
## 📊 테스트 결과 요약
|
||||
|
||||
| 구분 | 개수 | 비율 |
|
||||
|------|------|------|
|
||||
| ✅ PASS | 44 | 93.6% |
|
||||
| ⚠️ PARTIAL PASS | 3 | 6.4% |
|
||||
| ❌ FAIL | 0 | 0% |
|
||||
|
||||
---
|
||||
|
||||
## 🔴 HIGH 우선순위 수정 항목
|
||||
|
||||
### 1. BUG-CARDTRANS-001: 카드내역 일괄변경 선택 항목 인식 안됨
|
||||
|
||||
**위치**: `/accounting/card-transactions`
|
||||
**컴포넌트**: `src/components/accounting/CardTransactionInquiry/`
|
||||
**심각도**: HIGH
|
||||
|
||||
**증상**:
|
||||
- 테이블 전체선택 체크박스 클릭 → "6개 항목 선택됨" 표시
|
||||
- 계정과목명 드롭다운에서 "경비" 선택
|
||||
- 저장 버튼 클릭
|
||||
- **예상**: "6개의 사용 유형을 경비(으)로 변경하시겠습니까?" 확인 다이얼로그
|
||||
- **실제**: "항목 선택 필요 - 변경할 카드 사용 내역을 먼저 선택해주세요." 오류
|
||||
|
||||
**원인 분석**:
|
||||
- 일괄변경 저장 시 `selectedItems` 상태가 올바르게 전달되지 않음
|
||||
- 또는 저장 핸들러에서 선택 항목 체크 로직 오류
|
||||
|
||||
**수정 방향**:
|
||||
```typescript
|
||||
// 확인 필요한 부분
|
||||
1. selectedItems 상태가 일괄변경 컴포넌트에 올바르게 전달되는지
|
||||
2. 저장 핸들러에서 selectedItems.size > 0 체크 로직
|
||||
3. UniversalListPage 또는 IntegratedListTemplateV2의 선택 상태 동기화
|
||||
```
|
||||
|
||||
**예상 수정 파일**:
|
||||
- `src/components/accounting/CardTransactionInquiry/index.tsx`
|
||||
- `src/components/templates/UniversalListPage/index.tsx` (선택 상태 관련)
|
||||
|
||||
---
|
||||
|
||||
### 2. BUG-BOARD-001: 게시판 글쓰기 폼 미렌더링
|
||||
|
||||
**위치**: `/boards/{board_id}`
|
||||
**컴포넌트**: 동적 게시판 페이지
|
||||
**심각도**: HIGH
|
||||
|
||||
**증상**:
|
||||
- 게시판 목록 페이지에서 "글쓰기" 버튼 클릭
|
||||
- URL이 `?mode=new`로 변경됨
|
||||
- **예상**: 게시글 작성 폼 표시 (제목, 내용 입력 필드)
|
||||
- **실제**: 목록 화면 그대로 유지
|
||||
|
||||
**원인 분석**:
|
||||
- `mode=new` URL 파라미터 감지 후 폼 렌더링 로직 누락
|
||||
- 또는 조건부 렌더링 로직 오류
|
||||
|
||||
**수정 방향**:
|
||||
```typescript
|
||||
// 확인 필요한 부분
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
// mode === 'new' 일 때 작성 폼 렌더링
|
||||
if (mode === 'new') {
|
||||
return <BoardWriteForm />;
|
||||
}
|
||||
```
|
||||
|
||||
**예상 수정 파일**:
|
||||
- `src/app/[locale]/(protected)/boards/[boardId]/page.tsx`
|
||||
- 또는 해당 게시판 컴포넌트
|
||||
|
||||
---
|
||||
|
||||
### 3. BUG-BOARD-002: 게시판 수정 폼 미렌더링
|
||||
|
||||
**위치**: `/boards/{board_id}/{post_id}`
|
||||
**컴포넌트**: 게시판 상세 페이지
|
||||
**심각도**: HIGH
|
||||
|
||||
**증상**:
|
||||
- 게시글 상세 페이지에서 "수정" 버튼 클릭
|
||||
- URL이 `?mode=edit`로 변경됨
|
||||
- **예상**: 게시글 편집 폼 표시 (기존 내용 로드)
|
||||
- **실제**: 상세보기 화면 그대로 유지
|
||||
|
||||
**원인 분석**:
|
||||
- `mode=edit` URL 파라미터 감지 후 편집 폼 렌더링 로직 누락
|
||||
- 또는 조건부 렌더링 로직 오류
|
||||
|
||||
**수정 방향**:
|
||||
```typescript
|
||||
// 확인 필요한 부분
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
// mode === 'edit' 일 때 편집 폼 렌더링
|
||||
if (mode === 'edit') {
|
||||
return <BoardEditForm postData={postData} />;
|
||||
}
|
||||
```
|
||||
|
||||
**예상 수정 파일**:
|
||||
- `src/app/[locale]/(protected)/boards/[boardId]/[postId]/page.tsx`
|
||||
- 또는 해당 게시판 상세 컴포넌트
|
||||
|
||||
---
|
||||
|
||||
## 🟡 MEDIUM 우선순위 수정 항목
|
||||
|
||||
### 4. BUG-ATTSETTING-001: 자동 출퇴근 설정 저장 안됨
|
||||
|
||||
**위치**: `/settings/attendance-settings`
|
||||
**컴포넌트**: `src/components/settings/AttendanceSettings/` (추정)
|
||||
**심각도**: MEDIUM
|
||||
|
||||
**증상**:
|
||||
- GPS 출퇴근 활성화 → 부서 선택 → 반경 300M 설정
|
||||
- 자동 출퇴근 활성화
|
||||
- 저장 버튼 클릭
|
||||
- 페이지 새로고침
|
||||
- **예상**: 자동 출퇴근 체크박스 ON 상태 유지
|
||||
- **실제**: 자동 출퇴근 체크박스 OFF로 초기화됨
|
||||
- **참고**: GPS 출퇴근 설정(체크박스, 반경)은 정상 저장됨
|
||||
|
||||
**원인 분석**:
|
||||
- 자동 출퇴근 설정값이 API 호출에 포함되지 않음
|
||||
- 또는 백엔드에서 해당 필드 저장 누락
|
||||
|
||||
**수정 방향**:
|
||||
```typescript
|
||||
// 저장 API 호출 시 payload 확인
|
||||
const savePayload = {
|
||||
gpsEnabled: true,
|
||||
gpsRadius: 300,
|
||||
autoPunchEnabled: true, // ← 이 값이 포함되는지 확인
|
||||
...
|
||||
};
|
||||
```
|
||||
|
||||
**예상 수정 파일**:
|
||||
- `src/components/settings/AttendanceSettings/index.tsx`
|
||||
- `src/components/settings/AttendanceSettings/actions.ts`
|
||||
|
||||
---
|
||||
|
||||
## 🟢 LOW 우선순위 수정 항목
|
||||
|
||||
### 5. BUG-ATTSETTING-002: 저장 완료 토스트 미표시
|
||||
|
||||
**위치**: `/settings/attendance-settings`
|
||||
**심각도**: LOW
|
||||
|
||||
**증상**:
|
||||
- 저장 버튼 클릭 시 "출퇴근 설정이 저장되었습니다." 토스트 미표시
|
||||
- 콘솔 에러 없음, URL 유지됨
|
||||
|
||||
**수정 방향**:
|
||||
```typescript
|
||||
// 저장 성공 후 토스트 추가
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const handleSave = async () => {
|
||||
const result = await saveSettings(payload);
|
||||
if (result.success) {
|
||||
toast.success('출퇴근 설정이 저장되었습니다.');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 작업 체크리스트
|
||||
|
||||
### Phase 1: HIGH 우선순위 (즉시 수정) ✅ 완료
|
||||
|
||||
- [x] **카드내역 일괄변경 버그 수정** (2026-01-27 수정 완료)
|
||||
- [x] CardTransactionInquiry 컴포넌트 분석
|
||||
- [x] selectedItems 상태 흐름 추적
|
||||
- [x] UniversalListPage에 onSelectionChange 콜백 추가
|
||||
- [x] 테스트 검증
|
||||
|
||||
- [x] **게시판 글쓰기 폼 렌더링 수정** (2026-01-27 수정 완료)
|
||||
- [x] 동적 게시판 페이지 구조 분석
|
||||
- [x] mode=new 파라미터 처리 로직 추가
|
||||
- [x] DynamicBoardCreateForm 컴포넌트 분리 및 연결
|
||||
- [x] 테스트 검증
|
||||
|
||||
- [x] **게시판 수정 폼 렌더링 수정** (2026-01-27 수정 완료)
|
||||
- [x] 게시판 상세 페이지 구조 분석
|
||||
- [x] mode=edit 파라미터 처리 로직 추가
|
||||
- [x] DynamicBoardEditForm 컴포넌트 분리 및 연결
|
||||
- [x] 테스트 검증
|
||||
|
||||
### Phase 2: MEDIUM 우선순위 ✅ 완료
|
||||
|
||||
- [x] **자동 출퇴근 설정 저장 버그 수정** (2026-01-27 수정 완료)
|
||||
- [x] 저장 API payload 분석
|
||||
- [x] 백엔드 DB에 use_auto 컬럼 추가 (마이그레이션)
|
||||
- [x] 백엔드 Model/Request 수정
|
||||
- [x] 프론트엔드 API 연동 수정
|
||||
- [x] 테스트 검증
|
||||
|
||||
### Phase 3: LOW 우선순위 ✅ 완료
|
||||
|
||||
- [x] **저장 완료 토스트 추가** (이미 구현되어 있음)
|
||||
- [x] 저장 핸들러에 toast.success 이미 존재 (index.tsx:142)
|
||||
|
||||
---
|
||||
|
||||
## 📌 추가 확인 필요 사항
|
||||
|
||||
### 백엔드 이슈 (프론트 수정 범위 외)
|
||||
|
||||
| 이슈 | 위치 | 상태 |
|
||||
|------|------|------|
|
||||
| reference-box 500 에러 | `/api/v1/boards/reference` | 백엔드 확인 필요 |
|
||||
|
||||
### 기획 변경 사항
|
||||
|
||||
| 항목 | 변경 내용 | 조치 |
|
||||
|------|----------|------|
|
||||
| payment-history | 구독관리 페이지로 통합됨 | 테스트 시나리오 삭제/수정 |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 참고: 이미 수정된 항목
|
||||
|
||||
### bank-transactions key 중복 오류 (2026-01-27 수정 완료)
|
||||
|
||||
**수정 내용**: 입금/출금 통합 목록에서 React key 중복 오류
|
||||
**수정 파일**: `src/components/accounting/BankTransactionInquiry/actions.ts`
|
||||
**수정 방법**: ID 생성 시 거래 유형을 접두어로 추가
|
||||
```typescript
|
||||
// 기존: id: String(item.id)
|
||||
// 수정: id: `${item.type}-${item.id}`
|
||||
```
|
||||
|
||||
### BUG-CARDTRANS-001: 카드내역 일괄변경 선택 인식 (2026-01-27 수정 완료)
|
||||
|
||||
**수정 내용**: UniversalListPage에서 선택 상태 변경 시 외부 콜백 호출
|
||||
**수정 파일**:
|
||||
- `src/components/templates/UniversalListPage/types.ts` - onSelectionChange 타입 추가
|
||||
- `src/components/templates/UniversalListPage/index.tsx` - useEffect로 콜백 호출 추가
|
||||
|
||||
### BUG-BOARD-001/002: 게시판 글쓰기/수정 폼 미렌더링 (2026-01-27 수정 완료)
|
||||
|
||||
**수정 내용**: mode=new/edit URL 파라미터 처리 로직 추가
|
||||
**신규 컴포넌트**:
|
||||
- `src/components/board/DynamicBoard/DynamicBoardCreateForm.tsx` - 글쓰기 폼
|
||||
- `src/components/board/DynamicBoard/DynamicBoardEditForm.tsx` - 수정 폼
|
||||
|
||||
**수정 파일**:
|
||||
- `src/app/[locale]/(protected)/boards/[boardCode]/page.tsx` - mode=new 처리
|
||||
- `src/app/[locale]/(protected)/boards/[boardCode]/[postId]/page.tsx` - mode=edit 처리
|
||||
- `src/app/[locale]/(protected)/boards/[boardCode]/create/page.tsx` - 컴포넌트 재사용
|
||||
- `src/app/[locale]/(protected)/boards/[boardCode]/[postId]/edit/page.tsx` - 컴포넌트 재사용
|
||||
|
||||
### BUG-ATTSETTING-001: 자동 출퇴근 설정 저장 안됨 (2026-01-27 수정 완료)
|
||||
|
||||
**원인**: 백엔드 DB에 use_auto 컬럼이 없었음
|
||||
|
||||
**백엔드 수정 (sam-api)**:
|
||||
- `database/migrations/2026_01_27_144110_add_use_auto_to_attendance_settings.php` - 마이그레이션 생성
|
||||
- `app/Models/Tenants/AttendanceSetting.php` - fillable, casts에 use_auto 추가
|
||||
- `app/Http/Requests/V1/WorkSetting/UpdateAttendanceSettingRequest.php` - validation 규칙 추가
|
||||
|
||||
**프론트엔드 수정 (sam-react-prod)**:
|
||||
- `src/components/settings/AttendanceSettingsManagement/actions.ts` - API 타입 및 transform 함수 수정
|
||||
- `src/components/settings/AttendanceSettingsManagement/index.tsx` - 로드/저장 시 useAuto 필드 연동
|
||||
|
||||
---
|
||||
|
||||
**작성자**: Claude Code
|
||||
**상태**: 전체 완료 ✅
|
||||
**백엔드 배포 필요**: sam-api 프로젝트에서 `php artisan migrate` 실행 필요
|
||||
@@ -0,0 +1,84 @@
|
||||
# 품질인정심사 시스템 구현 체크리스트
|
||||
|
||||
> **경로**: `src/app/[locale]/(protected)/dev/quality-inspection/`
|
||||
> **작업일**: 2025-12-29
|
||||
> **담당**: 버디
|
||||
> **상태**: ✅ 완료
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 상태 관리 구현 ✅
|
||||
|
||||
- [x] 1.1 page.tsx에 필터 상태 추가 (년도, 분기, 검색어)
|
||||
- [x] 1.2 selectedReport 상태 추가
|
||||
- [x] 1.3 selectedRoute 상태 추가
|
||||
- [x] 1.4 필터링 로직 구현 (useMemo)
|
||||
|
||||
## Phase 2: 컴포넌트 Props 연동 ✅
|
||||
|
||||
- [x] 2.1 ReportList.tsx - onSelect 콜백 추가
|
||||
- [x] 2.2 RouteList.tsx - reports 데이터 + onSelect 콜백 추가
|
||||
- [x] 2.3 DocumentList.tsx - route 데이터 연동
|
||||
- [x] 2.4 Filters.tsx - 상태 콜백 연동
|
||||
|
||||
## Phase 3: Mock 데이터 통합 ✅
|
||||
|
||||
- [x] 3.1 types.ts에 통합 데이터 구조 정의
|
||||
- [x] 3.2 mockData.ts 생성 (계층 구조 데이터)
|
||||
- [x] 3.3 Report → Route → Document 연결 구조
|
||||
|
||||
## Phase 4: 문서 모달 연동 ✅
|
||||
|
||||
### 기존 문서 컴포넌트 (재사용)
|
||||
|
||||
| 문서 종류 | 기존 컴포넌트 | 상태 |
|
||||
|----------|--------------|------|
|
||||
| 수주서 | `orders/documents/OrderDocumentModal.tsx` | ✅ 있음 |
|
||||
| 작업일지 | `production/WorkerScreen/WorkLogModal.tsx` | ✅ 있음 |
|
||||
| 납품확인서 | `outbound/ShipmentManagement/documents/DeliveryConfirmation.tsx` | ✅ 있음 |
|
||||
| 출고증 | `outbound/ShipmentManagement/documents/ShippingSlip.tsx` | ✅ 있음 |
|
||||
|
||||
### 신규 문서 (양식 필요)
|
||||
|
||||
| 문서 종류 | 상태 | 비고 |
|
||||
|----------|------|------|
|
||||
| 수입검사 성적서 | ❌ 양식 필요 | 디자인 파일 대기 |
|
||||
| 중간검사 성적서 | ❌ 양식 필요 | 디자인 파일 대기 |
|
||||
| 제품검사 성적서 | ❌ 양식 필요 | 디자인 파일 대기 |
|
||||
| 품질관리서 | ❌ 양식 필요 | 디자인 파일 대기 |
|
||||
|
||||
### 모달 연동 작업
|
||||
|
||||
- [x] 4.1 InspectionModal에서 문서 타입별 분기 처리
|
||||
- [x] 4.2 기존 문서 컴포넌트 Placeholder 표시 (연동 예정 안내)
|
||||
- [x] 4.3 신규 문서는 Placeholder 표시 (양식 대기)
|
||||
|
||||
## Phase 5: UI 개선 ✅
|
||||
|
||||
- [x] 5.1 PageLayout 적용 → N/A (전체 높이 대시보드 레이아웃으로 별도 처리)
|
||||
- [x] 5.2 Filters.tsx 미사용 import 정리 → 미사용 import 없음 확인
|
||||
- [x] 5.3 반응형 레이아웃 검증 → grid-cols-12 + lg: 반응형 적용됨
|
||||
|
||||
---
|
||||
|
||||
## 진행 현황
|
||||
|
||||
| Phase | 상태 | 완료일 |
|
||||
|-------|------|--------|
|
||||
| Phase 1 | ✅ 완료 | 2025-12-29 |
|
||||
| Phase 2 | ✅ 완료 | 2025-12-29 |
|
||||
| Phase 3 | ✅ 완료 | 2025-12-29 |
|
||||
| Phase 4 | ✅ 완료 | 2025-12-29 |
|
||||
| Phase 5 | ✅ 완료 | 2025-12-29 |
|
||||
|
||||
---
|
||||
|
||||
## 참고 파일
|
||||
|
||||
```
|
||||
src/components/orders/documents/OrderDocumentModal.tsx
|
||||
src/components/production/WorkerScreen/WorkLogModal.tsx
|
||||
src/components/outbound/ShipmentManagement/documents/DeliveryConfirmation.tsx
|
||||
src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx
|
||||
src/components/process-management/ProcessWorkLogPreviewModal.tsx
|
||||
```
|
||||
362
claudedocs/dev/[IMPL-2026-01-23] full-page-inspection.md
Normal file
362
claudedocs/dev/[IMPL-2026-01-23] full-page-inspection.md
Normal file
@@ -0,0 +1,362 @@
|
||||
# 전체 79페이지 검수 체크리스트
|
||||
|
||||
> Created: 2026-01-23
|
||||
> 기준 문서: mode-navigation-full-checklist.md
|
||||
|
||||
## 검수 항목
|
||||
|
||||
| 항목 | 체크 내용 |
|
||||
|------|----------|
|
||||
| **URL 패턴** | `?mode=new`, `?mode=view`, `?mode=edit` 정확한가 |
|
||||
| **mode=view** | 수정하기/목록가기 버튼 존재, 동작, 데이터 표시 |
|
||||
| **mode=edit** | 취소/저장 버튼 존재, 동작, 데이터 표시, 수정 가능 |
|
||||
| **mode=new** | 등록 페이지 폼 정상 표시 |
|
||||
|
||||
## 범례
|
||||
|
||||
- ⬜ 미검수
|
||||
- ✅ 정상
|
||||
- ❌ 수정필요
|
||||
- ➖ 해당없음 (모달/인라인/조회전용)
|
||||
|
||||
---
|
||||
|
||||
## 🏠 기본 페이지 (2)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 1 | 대시보드 | `/ko/dashboard` | ➖ | ➖ | ➖ | ⬜ | |
|
||||
| 2 | 로그인 | `/ko/login` | ➖ | ➖ | ➖ | ⬜ | |
|
||||
|
||||
---
|
||||
|
||||
## 👥 인사관리 (7)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 3 | 부서관리 | `/ko/hr/department-management` | ➖ | ➖ | ➖ | ⬜ | 모달 |
|
||||
| 4 | 사원관리 | `/ko/hr/employee-management` | ✅ | ✅ | ✅ | ✅ | 정상 |
|
||||
| 5 | 근태관리 | `/ko/hr/attendance-management` | ➖ | ➖ | ➖ | ⬜ | 모달 |
|
||||
| 6 | 휴가관리 | `/ko/hr/vacation-management` | ➖ | ➖ | ➖ | ⬜ | 모달 |
|
||||
| 7 | 급여관리 | `/ko/hr/salary-management` | ➖ | ➖ | ➖ | ⬜ | 모달 |
|
||||
| 8 | 모바일출퇴근 | `/ko/hr/attendance` | ➖ | ➖ | ➖ | ⬜ | |
|
||||
| 9 | 카드관리 | `/ko/hr/card-management` | ✅ | ✅ | ❌ | ❌ | edit URL 미변경 |
|
||||
|
||||
---
|
||||
|
||||
## 💰 판매관리 (4)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 10 | 거래처관리 | `/ko/sales/client-management-sales-admin` | ❌ | ❌ | ✅ | ❌ | new오류, view URL누락 |
|
||||
| 11 | 견적관리 | `/ko/sales/quote-management` | ✅ | ✅ | ❌ | ❌ | edit 제목 "견적 수정 수정" 중복 |
|
||||
| 12 | 단가관리 | `/ko/sales/pricing-management` | ❌ | ➖ | ✅ | ❌ | new URL변경되지만 폼미표시 |
|
||||
| 13 | 수주관리 | `/ko/sales/order-management-sales` | ✅ | ✅ | ❌ | ❌ | edit URL /edit path기반 |
|
||||
|
||||
---
|
||||
|
||||
## 📦 기준정보관리 (2)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 14 | 품목기준관리 | `/ko/master-data/item-master-data-management` | ➖ | ➖ | ➖ | ⬜ | 설정 |
|
||||
| 15 | 공정관리 | `/ko/master-data/process-management` | ✅ | ✅ | ❌ | ❌ | edit 제목 "공정 수정 수정" 중복 |
|
||||
|
||||
---
|
||||
|
||||
## 🏭 생산관리 (3)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 16 | 품목관리 | `/ko/production/screen-production` | ✅ | ✅ | ✅ | ✅ | 정상 |
|
||||
| 17 | 작업지시관리 | `/ko/production/work-orders` | ❌ | ✅ | ❌ | ❌ | new 폼미표시, edit URL미변경 |
|
||||
| 18 | 작업실적조회 | `/ko/production/work-results` | ➖ | ⬜ | ➖ | ⬜ | 조회전용 |
|
||||
|
||||
---
|
||||
|
||||
## 📦 자재관리 (2)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 19 | 재고현황 | `/ko/material/stock-status` | ➖ | ⬜ | ➖ | ⬜ | 조회 |
|
||||
| 20 | 입고관리 | `/ko/material/receiving` | ➖ | ➖ | ➖ | ⬜ | 개발중 |
|
||||
|
||||
---
|
||||
|
||||
## 🔬 품질관리 (1)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 21 | 검사관리 | `/ko/quality/inspections` | ✅ | ➖ | ➖ | ✅ | 데이터없음, new 정상 |
|
||||
|
||||
---
|
||||
|
||||
## 📤 출고관리 (1)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 22 | 출하관리 | `/ko/outbound/shipments` | ✅ | ✅ | ❌ | ❌ | edit 제목 "출고 수정 () 수정" 중복 |
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 설정 (10)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 23 | 휴가정책 | `/ko/settings/leave-policy` | ➖ | ➖ | ➖ | ⬜ | 설정 |
|
||||
| 24 | 권한관리 | `/ko/settings/permissions` | ✅ | ✅ | ✅ | ✅ | view/edit 통합화면 |
|
||||
| 25 | 직급관리 | `/ko/settings/ranks` | ➖ | ➖ | ➖ | ⬜ | 인라인 |
|
||||
| 26 | 직책관리 | `/ko/settings/titles` | ➖ | ➖ | ➖ | ⬜ | 인라인 |
|
||||
| 27 | 근무일정 | `/ko/settings/work-schedule` | ➖ | ➖ | ➖ | ⬜ | 설정 |
|
||||
| 28 | 출퇴근관리 | `/ko/settings/attendance-settings` | ➖ | ➖ | ➖ | ⬜ | 설정 |
|
||||
| 29 | 계좌관리 | `/ko/settings/accounts` | ✅ | ✅ | ❌ | ❌ | edit URL미변경(mode=view유지) |
|
||||
| 30 | 알림설정 | `/ko/settings/notification-settings` | ➖ | ➖ | ➖ | ⬜ | 설정 |
|
||||
| 31 | 게시판관리 | `/ko/board/board-management` | ✅ | ✅ | ✅ | ✅ | 정상 |
|
||||
| 32 | 팝업관리 | `/ko/settings/popup-management` | ✅ | ✅ | ✅ | ✅ | 정상 |
|
||||
|
||||
---
|
||||
|
||||
## 📝 전자결재 (3)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 33 | 기안함 | `/ko/approval/draft` | ✅ | ➖ | ➖ | ✅ | 모달상세, new 정상 |
|
||||
| 34 | 결재함 | `/ko/approval/inbox` | ➖ | ➖ | ➖ | ⬜ | 모달 |
|
||||
| 35 | 참조함 | `/ko/approval/reference` | ➖ | ➖ | ➖ | ⬜ | 모달 |
|
||||
|
||||
---
|
||||
|
||||
## 💵 회계관리 (13)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 36 | 거래처관리 | `/ko/accounting/vendors` | ➖ | ⬜ | ⬜ | ⬜ | 등록없음 |
|
||||
| 37 | 매입관리 | `/ko/accounting/purchase` | ➖ | ⬜ | ⬜ | ⬜ | 등록없음 |
|
||||
| 38 | 매출관리 | `/ko/accounting/sales` | ✅ | ✅ | ❌ | ❌ | edit URL미변경(mode=view유지) |
|
||||
| 39 | 입금관리 | `/ko/accounting/deposits` | ✅ | ✅ | ✅ | ✅ | 정상 |
|
||||
| 40 | 출금관리 | `/ko/accounting/withdrawals` | ✅ | ✅ | ✅ | ✅ | 정상 |
|
||||
| 41 | 어음관리 | `/ko/accounting/bills` | ❌ | ✅ | ❌ | ❌ | new 제목중복"어음 등록 등록", edit URL미변경 |
|
||||
| 42 | 거래처원장 | `/ko/accounting/vendor-ledger` | ➖ | ➖ | ➖ | ⬜ | 조회전용 |
|
||||
| 43 | 일일일보 | `/ko/accounting/daily-report` | ➖ | ➖ | ➖ | ⬜ | 조회전용 |
|
||||
| 44 | 지출예상내역서 | `/ko/accounting/expected-expenses` | ➖ | ➖ | ➖ | ⬜ | 조회전용 |
|
||||
| 45 | 미수금현황 | `/ko/accounting/receivables-status` | ➖ | ➖ | ➖ | ⬜ | 조회전용 |
|
||||
| 46 | 입출금계좌조회 | `/ko/accounting/bank-transactions` | ➖ | ➖ | ➖ | ⬜ | 조회전용 |
|
||||
| 47 | 카드내역조회 | `/ko/accounting/card-transactions` | ✅ | ➖ | ➖ | ✅ | new정상, 상세는모달 |
|
||||
| 48 | 악성채권추심 | `/ko/accounting/bad-debt-collection` | ➖ | ⬜ | ⬜ | ⬜ | 등록없음 |
|
||||
|
||||
---
|
||||
|
||||
## 📝 게시판 (2)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 49 | 게시판목록 | `/ko/board` | ➖ | ➖ | ➖ | ⬜ | 선택페이지 |
|
||||
| 50 | 게시판상세 | `/ko/boards/[boardCode]` | ⬜ | ❌ | ⬜ | ❌ | view 404오류(라우트미구현) |
|
||||
|
||||
---
|
||||
|
||||
## 📊 보고서 (1)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 51 | 종합경영분석 | `/ko/reports/comprehensive-analysis` | ➖ | ➖ | ➖ | ⬜ | 분석전용 |
|
||||
|
||||
---
|
||||
|
||||
## 👤 계정/회사/구독 (4)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 52 | 계정정보 | `/ko/settings/account-info` | ➖ | ➖ | ⬜ | ⬜ | 수정만 |
|
||||
| 53 | 회사정보 | `/ko/company-info` | ➖ | ➖ | ⬜ | ⬜ | 수정만 |
|
||||
| 54 | 구독관리 | `/ko/subscription` | ➖ | ➖ | ➖ | ⬜ | 플랜선택 |
|
||||
| 55 | 결제내역 | `/ko/payment-history` | ➖ | ⬜ | ➖ | ⬜ | 상세만 |
|
||||
|
||||
---
|
||||
|
||||
## 📢 고객센터 (4)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 56 | 공지사항 | `/ko/customer-center/notices` | ➖ | ⬜ | ➖ | ⬜ | 상세만 |
|
||||
| 57 | 이벤트 | `/ko/customer-center/events` | ➖ | ⬜ | ➖ | ⬜ | 상세만 |
|
||||
| 58 | FAQ | `/ko/customer-center/faq` | ➖ | ➖ | ➖ | ⬜ | 조회전용 |
|
||||
| 59 | 1:1문의 | `/ko/customer-center/qna` | ❌ | ❌ | ⬜ | ❌ | new/view 화면미표시 |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 건설-프로젝트 (2)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 60 | 프로젝트관리 | `/ko/construction/project/management` | ➖ | ⬜ | ⬜ | ⬜ | 자동생성 |
|
||||
| 61 | 프로젝트실행 | `/ko/construction/project/execution-management` | ➖ | ⬜ | ➖ | ⬜ | 대시보드 |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 건설-입찰 (4)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 62 | 거래처관리 | `/ko/construction/project/bidding/partners` | ❌ | ✅ | ❌ | ❌ | new 제목중복, edit URL미변경 |
|
||||
| 63 | 현장설명회 | `/ko/construction/project/bidding/site-briefings` | ❌ | ➖ | ➖ | ❌ | new 제목중복, 데이터없음 |
|
||||
| 64 | 견적관리 | `/ko/construction/project/bidding/estimates` | ➖ | ⬜ | ⬜ | ⬜ | 등록없음 |
|
||||
| 65 | 입찰관리 | `/ko/construction/project/bidding` | ➖ | ⬜ | ⬜ | ⬜ | 자동생성 |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 건설-계약 (2)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 66 | 계약관리 | `/ko/construction/project/contract` | ➖ | ⬜ | ⬜ | ⬜ | 자동생성 |
|
||||
| 67 | 인수인계보고서 | `/ko/construction/project/contract/handover-report` | ➖ | ⬜ | ⬜ | ⬜ | 자동생성 |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 건설-발주 (3)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 68 | 현장관리 | `/ko/construction/order/site-management` | ➖ | ⬜ | ⬜ | ⬜ | 자동생성 |
|
||||
| 69 | 구조검토관리 | `/ko/construction/order/structure-review` | ❌ | ➖ | ➖ | ❌ | new 제목오류"상세수정", 데이터없음 |
|
||||
| 70 | 발주관리 | `/ko/construction/order/order-management` | ✅ | ❌ | ⬜ | ❌ | new정상, view오류발생 |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 건설-공사 (4)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 71 | 시공관리 | `/ko/construction/project/construction-management` | ➖ | ⬜ | ⬜ | ⬜ | 자동생성 |
|
||||
| 72 | 이슈관리 | `/ko/construction/project/issue-management` | ❌ | ✅ | ❌ | ❌ | new 제목중복"이슈 등록 등록", edit URL미변경 |
|
||||
| 73 | 공과관리 | `/ko/construction/project/utility-management` | ➖ | ➖ | ➖ | ⬜ | 자동생성 |
|
||||
| 74 | 작업인력현황 | `/ko/construction/project/worker-status` | ➖ | ➖ | ➖ | ⬜ | 조회 |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 건설-기성청구 (1)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 75 | 기성청구관리 | `/ko/construction/billing/progress-billing-management` | ➖ | ⬜ | ⬜ | ⬜ | 자동생성 |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 건설-기준정보 (4)
|
||||
|
||||
| # | 페이지 | URL | new | view | edit | 상태 | 비고 |
|
||||
|---|--------|-----|-----|------|------|------|------|
|
||||
| 76 | 카테고리관리 | `/ko/construction/order/base-info/categories` | ➖ | ➖ | ➖ | ⬜ | 인라인 |
|
||||
| 77 | 품목관리 | `/ko/construction/order/base-info/items` | ❌ | ✅ | ❌ | ❌ | new 제목중복"품목 등록 수정", edit URL미변경 |
|
||||
| 78 | 단가관리 | `/ko/construction/order/base-info/pricing` | ✅ | ➖ | ➖ | ✅ | new정상, 데이터없음 |
|
||||
| 79 | 노임관리 | `/ko/construction/order/base-info/labor` | ✅ | ➖ | ➖ | ✅ | new정상, 데이터없음 |
|
||||
|
||||
---
|
||||
|
||||
## 📊 요약
|
||||
|
||||
| 구분 | 전체 | URL기반 | 모달/인라인 | 자동생성 | 조회전용 |
|
||||
|------|------|---------|-------------|----------|----------|
|
||||
| 합계 | 79 | 34 | 16 | 13 | 16 |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 검수 진행 로그
|
||||
|
||||
### Round 3 검수 시작: 2026-01-23
|
||||
|
||||
| 시간 | 페이지# | 결과 | 문제점 |
|
||||
|------|---------|------|--------|
|
||||
| 10:30 | #4 사원관리 | ✅ | new/view/edit 모두 정상 |
|
||||
| 10:35 | #9 카드관리 | ❌ | 수정버튼 클릭시 URL ?mode=edit 미변경 |
|
||||
| 10:40 | #10 거래처관리 | ❌ | new 오류페이지, view URL ?mode=view 누락 |
|
||||
| 10:45 | #11 견적관리 | ❌ | edit 제목 "견적 수정 수정" 중복 |
|
||||
| 10:50 | #12 단가관리 | ❌ | new URL변경되지만 폼미표시, edit 정상 |
|
||||
| 10:55 | #13 수주관리 | ❌ | new/view 정상, edit URL /edit path기반 |
|
||||
| 11:00 | #15 공정관리 | ❌ | new/view 정상, edit 제목 "공정 수정 수정" 중복 |
|
||||
| 11:05 | #16 품목관리 | ✅ | new/view/edit 모두 정상 |
|
||||
| 11:10 | #17 작업지시관리 | ❌ | new 폼미표시, view 정상, edit URL미변경(mode=view유지) |
|
||||
| 11:15 | #21 검사관리 | ✅ | 데이터없음, new 정상, URL 패턴 정상 |
|
||||
| 11:20 | #22 출하관리 | ❌ | new/view 정상, edit 제목 "출고 수정 () 수정" 중복 |
|
||||
| 11:25 | #24 권한관리 | ✅ | view/edit 통합화면, 모두 정상 |
|
||||
| 11:30 | #29 계좌관리 | ❌ | new/view 정상, edit URL미변경(mode=view유지) |
|
||||
| 11:35 | #31 게시판관리 | ✅ | new/view/edit 모두 정상 |
|
||||
| 11:40 | #32 팝업관리 | ✅ | new/view/edit 모두 정상 |
|
||||
| 11:45 | #33 기안함 | ✅ | 모달상세, new 정상 |
|
||||
| 11:50 | #38 매출관리 | ❌ | new/view 정상, edit URL미변경(mode=view유지) |
|
||||
| 11:55 | #39 입금관리 | ✅ | new/view/edit 모두 정상 |
|
||||
| 12:00 | #40 출금관리 | ✅ | new/view/edit 모두 정상 |
|
||||
| 12:05 | #41 어음관리 | ❌ | new 제목중복"어음 등록 등록", edit URL미변경 |
|
||||
| 12:10 | #47 카드내역조회 | ✅ | new정상, 상세는모달 |
|
||||
| 12:15 | #50 게시판상세 | ❌ | view 404오류(라우트미구현) |
|
||||
| 12:20 | #59 1:1문의 | ❌ | new/view 화면미표시 |
|
||||
| 12:25 | #62 거래처관리(건설) | ❌ | new 제목중복, edit URL미변경 |
|
||||
| 12:30 | #63 현장설명회 | ❌ | new 제목중복, 데이터없음 |
|
||||
| 12:35 | #69 구조검토관리 | ❌ | new 제목오류"상세수정", 데이터없음 |
|
||||
| 12:40 | #70 발주관리 | ❌ | new정상, view오류발생 |
|
||||
| 12:45 | #72 이슈관리 | ❌ | new 제목중복"이슈 등록 등록", edit URL미변경 |
|
||||
| 12:50 | #77 품목관리(건설) | ❌ | new 제목중복"품목 등록 수정", edit URL미변경 |
|
||||
| 12:55 | #78 단가관리(건설) | ✅ | new정상, 데이터없음 |
|
||||
| 13:00 | #79 노임관리 | ✅ | new정상, 데이터없음 |
|
||||
|
||||
### 🎯 검수 완료 요약
|
||||
|
||||
**검수 완료**: 79페이지 중 URL 기반 CRUD 34페이지 검수 완료
|
||||
|
||||
**주요 버그 패턴**:
|
||||
1. **제목 중복 (11건)**: "X 등록 등록", "X 등록 수정", "X 상세 수정" 패턴
|
||||
2. **edit URL 미변경 (8건)**: 수정 버튼 클릭 시 URL이 mode=view로 유지
|
||||
3. **edit 필드 disabled (8건)**: 수정 모드인데 필드 비활성화
|
||||
4. **new 폼 미표시 (3건)**: URL 변경은 되지만 폼이 표시되지 않음
|
||||
5. **라우트 미구현 (1건)**: 404 오류
|
||||
|
||||
**정상 페이지**: #4, #16, #24, #31, #32, #33, #39, #40, #47, #78, #79 (11개)
|
||||
|
||||
---
|
||||
|
||||
## 🔧 수정 완료 (2026-01-23)
|
||||
|
||||
### 제목 중복 수정 (15건 → 완료)
|
||||
| 파일 | 수정 전 | 수정 후 |
|
||||
|------|---------|---------|
|
||||
| quoteConfig.ts | '견적 수정' | '견적' |
|
||||
| processConfig.ts | '공정 수정' | '공정' |
|
||||
| shipmentConfig.ts | '출고 수정' | '출고' |
|
||||
| workOrderConfig.ts | '작업지시 수정' | '작업지시' |
|
||||
| orderConfig.ts | '수주 수정' | '수주' |
|
||||
| BillDetail.tsx | '어음 등록' | '어음' |
|
||||
| WorkOrderEdit.tsx | '작업지시 수정 (번호)' | '작업지시 (번호)' |
|
||||
| SiteBriefingForm.tsx | '현장설명회 등록/수정' | '현장설명회' |
|
||||
| PartnerForm.tsx | '거래처 등록/수정' | '거래처' |
|
||||
| IssueDetailForm.tsx | '이슈 등록' | '이슈' |
|
||||
| StructureReviewDetailForm.tsx | titleMap 제거 | '구조검토' |
|
||||
| ItemDetailClient.tsx (건설) | titleMap 제거 | '품목' |
|
||||
| BiddingDetailForm.tsx | '입찰 상세/수정' | '입찰' |
|
||||
| EstimateDetailForm.tsx | '견적 수정' | '견적' |
|
||||
| ContractDetailForm.tsx | '계약 등록/변경 계약서 생성' | '계약/변경 계약서' |
|
||||
|
||||
### edit URL 미변경 수정 (8건 → 완료)
|
||||
| 파일 | 수정 내용 |
|
||||
|------|----------|
|
||||
| IntegratedDetailTemplate/index.tsx | handleEdit에서 router.push 추가 (글로벌 수정) |
|
||||
| AccountDetail.tsx | handleEdit에서 router.push 추가 |
|
||||
|
||||
### view URL 누락 수정 (1건 → 완료)
|
||||
| 파일 | 수정 내용 |
|
||||
|------|----------|
|
||||
| client-management-sales-admin/page.tsx | handleView에 `?mode=view` 추가 |
|
||||
|
||||
### mode=new 폼 미표시 수정 (3건 → 완료)
|
||||
| 파일 | 수정 내용 |
|
||||
|------|----------|
|
||||
| pricing-management/page.tsx | `?mode=new` 시 품목 선택 안내 표시 |
|
||||
| work-orders/page.tsx | `?mode=new` 시 WorkOrderCreate 렌더링 |
|
||||
| qna/page.tsx | `?mode=new` 시 InquiryDetailClientV2 렌더링 |
|
||||
|
||||
### 라우트 미구현 수정 (1건 → 완료)
|
||||
| 파일 | 수정 내용 |
|
||||
|------|----------|
|
||||
| board/[boardCode]/page.tsx | 누락된 라우트 생성 (게시글 목록 + mode=new 처리)
|
||||
|
||||
---
|
||||
163
claudedocs/dev/[PLAN-2026-02-03] claude-config-optimization.md
Normal file
163
claudedocs/dev/[PLAN-2026-02-03] claude-config-optimization.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# Claude 설정 최적화 계획
|
||||
|
||||
**날짜**: 2026-02-03
|
||||
**목적**: 토큰 효율성 개선 + 규칙 분리 + 자동화 강화
|
||||
|
||||
---
|
||||
|
||||
## 배경 (왜 이 작업을 하는가?)
|
||||
|
||||
### 문제 발견
|
||||
- `~/.claude/` 에 **16개 MD 파일 (3,549줄, ~119KB)** 가 매 대화마다 전부 로드됨
|
||||
- SAM 프로젝트 전용 규칙이 글로벌에 있어 다른 프로젝트에도 불필요하게 전파
|
||||
- 비즈니스 분석용 파일(BUSINESS_PANEL, RESEARCH_CONFIG 등)이 일상 개발에도 로드
|
||||
- type-check 같은 프로세스가 MD 규칙으로만 존재 → 강제성 없음
|
||||
|
||||
### 기대 효과
|
||||
- 매 대화 토큰 사용량 감소 (불필요한 ~1,500줄 절약)
|
||||
- SAM 규칙과 범용 규칙의 명확한 분리
|
||||
- type-check 자동화로 실수 방지 강화
|
||||
|
||||
---
|
||||
|
||||
## 작업 계획
|
||||
|
||||
### 1단계: 프로젝트 CLAUDE.md 분리
|
||||
**상태**: 🔄 진행 중
|
||||
|
||||
**작업 내용**:
|
||||
- `~/.claude/RULES.md`에서 SAM 전용 규칙 추출
|
||||
- 프로젝트 루트에 `CLAUDE.md` 생성 (SAM 전용 규칙)
|
||||
- 글로벌 `RULES.md`는 범용 규칙만 남김
|
||||
|
||||
**SAM 전용으로 분류된 규칙들** (프로젝트 CLAUDE.md로 이동):
|
||||
| 섹션 | 이유 |
|
||||
|------|------|
|
||||
| Common Table Standards | SAM ERP 테이블 패턴 |
|
||||
| Document Table Merging | SAM 성적서/보고서 전용 |
|
||||
| Page Layout Standards | SAM AuthenticatedLayout 구조 |
|
||||
| Design Popup Policy | SAM Radix UI 스택 |
|
||||
| Build Policy | SAM 빌드 정책 |
|
||||
| Client Component 사용 원칙 | SAM 폐쇄형 ERP 특성 |
|
||||
| SAM Project Structure | SAM 경로 구조 |
|
||||
| Backend API Analysis Policy | SAM 백엔드 연동 |
|
||||
| Test URL Documentation Rules | SAM 테스트 URL |
|
||||
| User Environment Knowledge | 사용자 환경 특화 |
|
||||
| HttpOnly Cookie API Communication | SAM 인증 패턴 |
|
||||
| Radix UI Select Bug | SAM 기술 스택 이슈 |
|
||||
| React → Next.js Migration | SAM 마이그레이션 |
|
||||
| Large File Migration Workflow | SAM 마이그레이션 |
|
||||
| Component Pattern Reuse | SAM 컴포넌트 패턴 |
|
||||
| 기획서/스크린샷 기반 UI 구현 | SAM 기획서 프로세스 |
|
||||
|
||||
**글로벌에 남는 규칙들** (범용):
|
||||
| 섹션 | 이유 |
|
||||
|------|------|
|
||||
| Priority System | 모든 프로젝트 공통 |
|
||||
| Scope Discipline (일반 부분) | 모든 프로젝트 공통 |
|
||||
| Implementation Completeness | 모든 프로젝트 공통 |
|
||||
| Failure Investigation | 모든 프로젝트 공통 |
|
||||
| Professional Honesty | 모든 프로젝트 공통 |
|
||||
| Safety Rules | 모든 프로젝트 공통 |
|
||||
| Workflow & Session | 모든 프로젝트 공통 |
|
||||
| File & Code Organization | 모든 프로젝트 공통 |
|
||||
| Workspace Hygiene | 모든 프로젝트 공통 |
|
||||
| Git Rules | 모든 프로젝트 공통 |
|
||||
| Claude-Specific Rules | 모든 프로젝트 공통 |
|
||||
| Quick Reference | 모든 프로젝트 공통 |
|
||||
|
||||
---
|
||||
|
||||
### 2단계: 가끔 쓰는 MD 파일 임포트 해제
|
||||
**상태**: ⏳ 대기
|
||||
|
||||
**작업 내용**:
|
||||
- `~/.claude/CLAUDE.md`에서 `@` 임포트 줄 제거 (파일 자체는 유지)
|
||||
|
||||
**제거 대상 임포트** (일상 SAM 개발에 불필요):
|
||||
| 파일 | 줄 수 | 용도 |
|
||||
|------|-------|------|
|
||||
| @BUSINESS_PANEL_EXAMPLES.md | 278줄 | /sc:business-panel 전용 |
|
||||
| @BUSINESS_SYMBOLS.md | 211줄 | /sc:business-panel 전용 |
|
||||
| @MODE_Business_Panel.md | 334줄 | /sc:business-panel 전용 |
|
||||
| @RESEARCH_CONFIG.md | 445줄 | /sc:research 전용 |
|
||||
| @MODE_DeepResearch.md | 57줄 | /sc:research 전용 |
|
||||
| @MODE_Brainstorming.md | 43줄 | 가끔 사용 |
|
||||
| @MODE_Introspection.md | 38줄 | 거의 안 씀 |
|
||||
| @MODE_Token_Efficiency.md | 74줄 | 컨텍스트 압박 시만 |
|
||||
| **합계** | **~1,480줄** | **절약** |
|
||||
|
||||
**유지 대상 임포트**:
|
||||
| 파일 | 이유 |
|
||||
|------|------|
|
||||
| @PRINCIPLES.md | 핵심 엔지니어링 원칙 |
|
||||
| @RULES.md | 범용 행동 규칙 |
|
||||
| @GIT_POLICY.md | Git 워크플로우 |
|
||||
| @FLAGS.md | 모드 활성화 플래그 |
|
||||
| @MODE_Task_Management.md | 멀티스텝 작업 관리 |
|
||||
| @MODE_Orchestration.md | 도구 선택 최적화 |
|
||||
|
||||
---
|
||||
|
||||
### 3단계: Hooks 추가
|
||||
**상태**: ⏳ 대기
|
||||
|
||||
**작업 내용**:
|
||||
- `.claude/settings.local.json`에 PostToolUse hook 추가
|
||||
- Write/Edit 후 자동 `npx tsc --noEmit` 실행
|
||||
|
||||
---
|
||||
|
||||
### 4단계: 중복 제거
|
||||
**상태**: ⏳ 대기
|
||||
|
||||
**작업 내용**:
|
||||
- RULES.md의 "Git Rules" 섹션과 GIT_POLICY.md 간 중복 정리
|
||||
- RULES.md에는 "git 올려줘" 단축 명령 + 기본 원칙만 남김
|
||||
- 상세 브랜치 전략/커밋 규칙은 GIT_POLICY.md로 통합
|
||||
- COMPLEX_TASK_PROTOCOL.md는 RULES.md의 Checklist-Driven Development와 중복 → 정리
|
||||
|
||||
---
|
||||
|
||||
## 진행 기록
|
||||
|
||||
| 단계 | 상태 | 완료일 | 비고 |
|
||||
|------|------|--------|------|
|
||||
| 1단계: 프로젝트 CLAUDE.md 분리 | ✅ | 2026-02-03 | SAM 전용 16개 섹션 이동 |
|
||||
| 2단계: MD 임포트 해제 | ✅ | 2026-02-03 | 9개 파일 임포트 해제 (~1,759줄) |
|
||||
| 3단계: Hooks 추가 | ✅ | 2026-02-03 | PostToolUse typecheck hook |
|
||||
| 4단계: 중복 제거 | ✅ | 2026-02-03 | GIT_POLICY.md에서 87줄 중복 제거 |
|
||||
|
||||
---
|
||||
|
||||
## 최종 결과
|
||||
|
||||
### 토큰 절약 수치
|
||||
```
|
||||
변경 전: 3,549줄 (전부 글로벌, 매 대화 로드)
|
||||
변경 후: 1,193줄 (글로벌 916 + 프로젝트 277)
|
||||
절약량: 2,356줄 (~66% 감소)
|
||||
```
|
||||
|
||||
### 다른 프로젝트에서의 효과
|
||||
```
|
||||
변경 전: 3,549줄 (SAM 규칙 포함)
|
||||
변경 후: 916줄 (범용 규칙만)
|
||||
절약량: 2,633줄 (~74% 감소)
|
||||
```
|
||||
|
||||
### 변경된 파일 목록
|
||||
| 파일 | 작업 |
|
||||
|------|------|
|
||||
| `프로젝트/CLAUDE.md` | 신규 생성 (SAM 전용 규칙) |
|
||||
| `~/.claude/CLAUDE.md` | 임포트 9개 해제 |
|
||||
| `~/.claude/RULES.md` | SAM 규칙 제거 (1017→258줄) |
|
||||
| `~/.claude/GIT_POLICY.md` | 중복 섹션 제거 (394→307줄) |
|
||||
| `.claude/hooks/typecheck-after-edit.sh` | 신규 생성 |
|
||||
| `.claude/settings.local.json` | hooks 설정 추가 |
|
||||
|
||||
### Hook 동작 방식
|
||||
- **트리거**: Write/Edit 도구 사용 후
|
||||
- **대상**: .ts/.tsx 파일만
|
||||
- **실행**: `npx tsc --noEmit` (30초 타임아웃)
|
||||
- **에러 시**: Claude에 타입 에러 피드백 → 자동 수정 시도
|
||||
155
claudedocs/dev/[PLAN] detail-page-pattern-classification.md
Normal file
155
claudedocs/dev/[PLAN] detail-page-pattern-classification.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# 상세/등록/수정 페이지 패턴 분류표
|
||||
|
||||
> Chrome DevTools MCP로 직접 확인한 결과 기반 (2026-01-19)
|
||||
|
||||
## 패턴 분류 기준
|
||||
|
||||
### 1️⃣ 페이지 형태 - 하단 버튼 (표준 패턴)
|
||||
- URL이 변경되며 별도 페이지로 이동
|
||||
- 버튼 위치: **하단** (좌: 목록/취소, 우: 삭제/수정/저장)
|
||||
- **IntegratedDetailTemplate 적용 대상**
|
||||
|
||||
### 2️⃣ 페이지 형태 - 상단 버튼
|
||||
- URL이 변경되며 별도 페이지로 이동
|
||||
- 버튼 위치: **상단**
|
||||
- IntegratedDetailTemplate 확장 필요 (`buttonPosition="top"`)
|
||||
|
||||
### 3️⃣ 모달 형태
|
||||
- URL 변경 없음, Dialog/Modal로 표시
|
||||
- **IntegratedDetailTemplate 적용 제외**
|
||||
|
||||
### 4️⃣ 인라인 입력 형태
|
||||
- 리스트 페이지 내에서 직접 입력/수정
|
||||
- **IntegratedDetailTemplate 적용 제외**
|
||||
|
||||
### 5️⃣ DynamicForm 형태
|
||||
- API 기반 동적 폼 생성
|
||||
- IntegratedDetailTemplate의 `renderForm` prop으로 분기 처리
|
||||
|
||||
---
|
||||
|
||||
## 📄 페이지 형태 - 하단 버튼 (통합 대상)
|
||||
|
||||
| 도메인 | 페이지 | URL 패턴 | 상태 |
|
||||
|--------|--------|----------|------|
|
||||
| **설정** | 계좌관리 | `/settings/accounts/[id]`, `/new` | ✅ 이미 마이그레이션 완료 |
|
||||
| **설정** | 카드관리 | `/hr/card-management/[id]`, `/new` | ✅ 이미 마이그레이션 완료 |
|
||||
| **설정** | 팝업관리 | `/settings/popup-management/[id]`, `/new` | 🔄 대상 |
|
||||
| **설정** | 게시판관리 | `/board/board-management/[id]`, `/new` | 🔄 대상 |
|
||||
| **기준정보** | 공정관리 | `/master-data/process-management/[id]`, `/new` | 🔄 대상 |
|
||||
| **판매** | 거래처관리 | `/sales/client-management-sales-admin/[id]`, `/new` | 🔄 대상 |
|
||||
| **판매** | 견적관리 | `/sales/quote-management/[id]`, `/new` | 🔄 대상 |
|
||||
| **판매** | 수주관리 | `/sales/order-management-sales/[id]`, `/new` | 🔄 대상 |
|
||||
| **품질** | 검사관리 | `/quality/inspections/[id]`, `/new` | 🔄 대상 |
|
||||
| **출고** | 출하관리 | `/outbound/shipments/[id]`, `/new` | 🔄 대상 |
|
||||
| **고객센터** | 공지사항 | `/customer-center/notices/[id]` | 🔄 대상 |
|
||||
| **고객센터** | 이벤트 | `/customer-center/events/[id]` | 🔄 대상 |
|
||||
|
||||
---
|
||||
|
||||
## 📄 페이지 형태 - 상단 버튼 (확장 필요)
|
||||
|
||||
| 도메인 | 페이지 | URL 패턴 | 버튼 구성 | 비고 |
|
||||
|--------|--------|----------|-----------|------|
|
||||
| **회계** | 거래처관리 | `/accounting/vendors/[id]`, `/new` | 목록/삭제/수정 | 다중 섹션 구조 |
|
||||
| **회계** | 매출관리 | `/accounting/sales/[id]`, `/new` | - | 🔄 대상 |
|
||||
| **회계** | 매입관리 | `/accounting/purchase/[id]` | - | 🔄 대상 |
|
||||
| **회계** | 입금관리 | `/accounting/deposits/[id]` | - | 🔄 대상 |
|
||||
| **회계** | 출금관리 | `/accounting/withdrawals/[id]` | - | 🔄 대상 |
|
||||
| **회계** | 어음관리 | `/accounting/bills/[id]`, `/new` | - | 🔄 대상 |
|
||||
| **회계** | 악성채권 | `/accounting/bad-debt-collection/[id]`, `/new` | - | 🔄 대상 |
|
||||
| **전자결재** | 기안함 (임시저장) | `/approval/draft/new?id=:id&mode=edit` | 상세/삭제/상신/저장 | 복잡한 섹션 구조 |
|
||||
|
||||
---
|
||||
|
||||
## 🔲 모달 형태 (통합 제외)
|
||||
|
||||
| 도메인 | 페이지 | 모달 컴포넌트 | 비고 |
|
||||
|--------|--------|--------------|------|
|
||||
| **설정** | 직급관리 | `RankDialog.tsx` | 인라인 입력 + 수정 모달 |
|
||||
| **설정** | 직책관리 | `TitleDialog.tsx` | 인라인 입력 + 수정 모달 |
|
||||
| **인사** | 부서관리 | `DepartmentDialog.tsx` | 트리 구조 |
|
||||
| **인사** | 근태관리 | `AttendanceInfoDialog.tsx` | 모달로 등록 |
|
||||
| **인사** | 휴가관리 | `VacationRequestDialog.tsx` | 모달로 등록/조정 |
|
||||
| **인사** | 급여관리 | `SalaryDetailDialog.tsx` | 모달로 상세 |
|
||||
| **전자결재** | 기안함 (결재대기) | 품의서 상세 Dialog | 상세만 모달 |
|
||||
| **건설** | 카테고리관리 | `CategoryDialog.tsx` | 모달로 등록/수정 |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 DynamicForm 형태 (renderForm 분기)
|
||||
|
||||
| 도메인 | 페이지 | URL 패턴 | 비고 |
|
||||
|--------|--------|----------|------|
|
||||
| **품목** | 품목관리 | `/items/[id]` | `DynamicItemForm` 사용 |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 특수 케이스 (개별 처리 필요)
|
||||
|
||||
| 도메인 | 페이지 | URL 패턴 | 특이사항 |
|
||||
|--------|--------|----------|----------|
|
||||
| **설정** | 권한관리 | `/settings/permissions/[id]`, `/new` | Matrix UI, 복잡한 구조 |
|
||||
| **인사** | 사원관리 | `/hr/employee-management/[id]`, `/new` | 40+ 필드, 탭 구조 |
|
||||
| **게시판** | 게시글 | `/board/[boardCode]/[postId]` | 동적 게시판 |
|
||||
| **건설** | 다수 페이지 | `/construction/...` | 별도 분류 필요 |
|
||||
|
||||
---
|
||||
|
||||
## 📊 통합 우선순위
|
||||
|
||||
### Phase 1: 단순 CRUD (우선 작업)
|
||||
1. 팝업관리
|
||||
2. 게시판관리
|
||||
3. 공정관리
|
||||
4. 공지사항/이벤트
|
||||
|
||||
### Phase 2: 중간 복잡도
|
||||
1. 판매 > 거래처관리
|
||||
2. 판매 > 견적관리
|
||||
3. 품질 > 검사관리
|
||||
4. 출고 > 출하관리
|
||||
|
||||
### Phase 3: 회계 도메인 (상단 버튼 확장 후)
|
||||
1. 회계 > 거래처관리
|
||||
2. 회계 > 매출/매입/입금/출금
|
||||
3. 회계 > 어음/악성채권
|
||||
|
||||
### 제외 (개별 유지)
|
||||
- 권한관리 (Matrix UI)
|
||||
- 사원관리 (40+ 필드)
|
||||
- 부서관리 (트리 구조)
|
||||
- 전자결재 (복잡한 워크플로우)
|
||||
- DynamicForm 페이지 (renderForm 분기)
|
||||
- 모달 형태 페이지들
|
||||
|
||||
---
|
||||
|
||||
## IntegratedDetailTemplate 확장 필요 Props
|
||||
|
||||
```typescript
|
||||
interface IntegratedDetailTemplateProps {
|
||||
// 기존 props...
|
||||
|
||||
// 버튼 위치 제어
|
||||
buttonPosition?: 'top' | 'bottom'; // default: 'bottom'
|
||||
|
||||
// 뒤로가기 버튼 표시 여부
|
||||
showBackButton?: boolean; // default: true
|
||||
|
||||
// 상단 버튼 커스텀 (문서 결재 등)
|
||||
headerActions?: ReactNode;
|
||||
|
||||
// 다중 섹션 지원
|
||||
sections?: Array<{
|
||||
title: string;
|
||||
fields: FieldConfig[];
|
||||
}>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 작성일
|
||||
- 최초 작성: 2026-01-19
|
||||
- Chrome DevTools MCP 확인 완료
|
||||
342
claudedocs/dev/[REF-2026-02-19] todo-issue-tracker.md
Normal file
342
claudedocs/dev/[REF-2026-02-19] todo-issue-tracker.md
Normal file
@@ -0,0 +1,342 @@
|
||||
# SAM ERP TODO/FIXME 이슈 트래커
|
||||
|
||||
> 자동 생성: 2026-02-19
|
||||
> 총 건수: 102건
|
||||
> 분류: A~F 카테고리
|
||||
> 검색 범위: `src/` 디렉토리 내 `*.ts`, `*.tsx` 파일
|
||||
|
||||
---
|
||||
|
||||
## 요약
|
||||
|
||||
| 카테고리 | 건수 | 담당 | 비고 |
|
||||
|----------|------|------|------|
|
||||
| A. 백엔드 API 연동 대기 | 55 | 백엔드팀 | API 완성 시 해결 |
|
||||
| B. 백엔드 필드 추가 대기 | 10 | 백엔드팀 | DB/API 스키마 추가 |
|
||||
| C. UI/기능 구현 대기 | 16 | 프론트팀 | 백로그 |
|
||||
| D. Phase 2 / 장기 과제 | 10 | 전체 | 로드맵 반영 |
|
||||
| E. CEO 대시보드 목업 데이터 | 4 | 프론트팀 | 대시보드 API 연동 시 |
|
||||
| F. 기타 | 7 | - | - |
|
||||
|
||||
---
|
||||
|
||||
## A. 백엔드 API 연동 대기 (55건)
|
||||
|
||||
### 모듈별 분류
|
||||
|
||||
#### 품목기준관리 (Item Master Store) - 16건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/stores/item-master/useItemMasterStore.ts` | 97 | `// TODO: API 호출` (createPage - 페이지 생성) |
|
||||
| `src/stores/item-master/useItemMasterStore.ts` | 160 | `// TODO: API 호출` (updatePage - 페이지 수정) |
|
||||
| `src/stores/item-master/useItemMasterStore.ts` | 214 | `// TODO: API 호출` (deletePage - 페이지 삭제) |
|
||||
| `src/stores/item-master/useItemMasterStore.ts` | 289 | `// TODO: API 호출` (createSection - 섹션 생성) |
|
||||
| `src/stores/item-master/useItemMasterStore.ts` | 335 | `// TODO: API 호출` (updateSection - 섹션 수정) |
|
||||
| `src/stores/item-master/useItemMasterStore.ts` | 370 | `// TODO: API 호출` (deleteSection - 섹션 삭제) |
|
||||
| `src/stores/item-master/useItemMasterStore.ts` | 423 | `// TODO: API 호출하여 서버에도 순서 저장` (reorderSections) |
|
||||
| `src/stores/item-master/useItemMasterStore.ts` | 460 | `// TODO: API 호출` (createField - 필드 생성) |
|
||||
| `src/stores/item-master/useItemMasterStore.ts` | 522 | `// TODO: API 호출` (updateField - 필드 수정) |
|
||||
| `src/stores/item-master/useItemMasterStore.ts` | 552 | `// TODO: API 호출` (deleteField - 필드 삭제) |
|
||||
| `src/stores/item-master/useItemMasterStore.ts` | 587 | `// TODO: API 호출` (updateFieldOptions - 필드 옵션 수정) |
|
||||
| `src/stores/item-master/useItemMasterStore.ts` | 640 | `// TODO: API 호출하여 서버에도 순서 저장` (reorderFields) |
|
||||
| `src/stores/item-master/useItemMasterStore.ts` | 677 | `// TODO: API 호출` (createTab - 탭 생성) |
|
||||
| `src/stores/item-master/useItemMasterStore.ts` | 707 | `// TODO: API 호출` (updateTab - 탭 수정) |
|
||||
| `src/stores/item-master/useItemMasterStore.ts` | 726 | `// TODO: API 호출` (deleteTab - 탭 삭제) |
|
||||
| `src/components/items/ItemMasterDataManagement/hooks/usePageManagement.ts` | 157 | `// TODO: 원본 페이지의 섹션들도 복제 필요 (별도 API 호출)` |
|
||||
|
||||
#### 회계 (Accounting) - 19건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/components/accounting/GiftCertificateManagement/actions.ts` | 32 | `// TODO: 실제 API 연동 시 교체` (getGiftCertificates) |
|
||||
| `src/components/accounting/GiftCertificateManagement/actions.ts` | 45 | `// TODO: 실제 API 연동 시 교체` (getGiftCertificateDetail) |
|
||||
| `src/components/accounting/GiftCertificateManagement/actions.ts` | 58 | `// TODO: 실제 API 연동 시 교체` (createGiftCertificate) |
|
||||
| `src/components/accounting/GiftCertificateManagement/actions.ts` | 86 | `// TODO: 실제 API 연동 시 교체` (updateGiftCertificate) |
|
||||
| `src/components/accounting/GiftCertificateManagement/actions.ts` | 113 | `// TODO: 실제 API 연동 시 교체` (deleteGiftCertificate) |
|
||||
| `src/components/accounting/GiftCertificateManagement/actions.ts` | 136 | `// TODO: 실제 API 연동 시 교체` (useGiftCertificate) |
|
||||
| `src/components/accounting/TaxInvoiceManagement/actions.ts` | 24 | `// TODO: 실제 API 연동 시 Mock 제거` (Mock 데이터) |
|
||||
| `src/components/accounting/TaxInvoiceManagement/actions.ts` | 44 | `// TODO: 실제 API 연동 시 아래 코드로 교체` (getTaxInvoices) |
|
||||
| `src/components/accounting/TaxInvoiceManagement/actions.ts` | 66 | `// TODO: 실제 API 연동 시 아래 코드로 교체` (getTaxInvoiceDetail) |
|
||||
| `src/components/accounting/TaxInvoiceManagement/actions.ts` | 99 | `// TODO: 실제 API 연동 시 Mock 제거` (세금계산서 상세) |
|
||||
| `src/components/accounting/TaxInvoiceManagement/actions.ts` | 115 | `// TODO: 실제 API 연동 시 아래 코드로 교체` (deleteTaxInvoice) |
|
||||
| `src/components/accounting/TaxInvoiceIssuance/actions.ts` | 36 | `// TODO: 실제 API 연동 시 아래 코드로 교체` (getTaxInvoices) |
|
||||
| `src/components/accounting/TaxInvoiceIssuance/actions.ts` | 59 | `// TODO: 실제 API 연동 시 아래 코드로 교체` (getTaxInvoiceDetail) |
|
||||
| `src/components/accounting/TaxInvoiceIssuance/actions.ts` | 86 | `// TODO: 실제 API 연동 시 아래 코드로 교체` (issueTaxInvoice) |
|
||||
| `src/components/accounting/TaxInvoiceIssuance/actions.ts` | 112 | `// TODO: 실제 API 연동 시 아래 코드로 교체` (sendEmail) |
|
||||
| `src/components/accounting/TaxInvoiceIssuance/actions.ts` | 127 | `// TODO: 실제 API 연동 시 교체` (cancelTaxInvoice) |
|
||||
| `src/components/accounting/TaxInvoiceIssuance/index.tsx` | 184 | `// TODO: 실제 API 연동 시 필터 조건으로 getTaxInvoices 호출` |
|
||||
| `src/components/accounting/PurchaseManagement/index.tsx` | 232 | `// TODO: API 호출로 저장` |
|
||||
| `src/components/accounting/SalesManagement/index.tsx` | 270 | `// TODO: API 호출로 저장` |
|
||||
|
||||
#### 건설/사업 (Construction/Business) - 9건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/components/business/construction/progress-billing/actions.ts` | 196 | `// TODO: 실제 API 호출로 대체` (getProgressBillings) |
|
||||
| `src/components/business/construction/progress-billing/actions.ts` | 230 | `// TODO: 실제 API 호출로 대체` (getProgressBillingDetail) |
|
||||
| `src/components/business/construction/progress-billing/actions.ts` | 263 | `// TODO: 실제 API 호출로 대체` (saveProgressBilling) |
|
||||
| `src/components/business/construction/progress-billing/actions.ts` | 292 | `// TODO: 실제 API 호출로 대체` (deleteProgressBilling) |
|
||||
| `src/components/business/construction/progress-billing/actions.ts` | 300 | `// TODO: 매출 자동 등록 API 호출` |
|
||||
| `src/components/business/construction/progress-billing/hooks/useProgressBillingDetailForm.ts` | 92 | `// TODO: API 호출` (handleSave) |
|
||||
| `src/components/business/construction/progress-billing/hooks/useProgressBillingDetailForm.ts` | 111 | `// TODO: API 호출` (handleDelete) |
|
||||
| `src/components/business/construction/progress-billing/ProgressBillingDetailForm.tsx` | 77 | `// TODO: API 호출` (handleDelete) |
|
||||
| `src/components/business/construction/management/actions.ts` | 20 | `// TODO: 실제 API 연동 시 구현` (프로젝트 관리 전체) |
|
||||
|
||||
#### 건설/현장관리 (Site Management) - 4건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/components/business/construction/structure-review/StructureReviewDetailForm.tsx` | 154 | `// TODO: API 연동` (handleSave) |
|
||||
| `src/components/business/construction/structure-review/StructureReviewDetailClientV2.tsx` | 76 | `// TODO: API 연동` (handleSave) |
|
||||
| `src/components/business/construction/site-management/SiteDetailForm.tsx` | 156 | `// TODO: API 연동` (handleSave) |
|
||||
| `src/components/business/construction/site-management/SiteDetailClientV2.tsx` | 68 | `// TODO: API 연동` (handleSave) |
|
||||
|
||||
#### HR (인사관리) - 2건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/app/[locale]/(protected)/hr/documents/page.tsx` | 73 | `// TODO: 백엔드 API 구현 필요` (인사서류함) |
|
||||
| `src/app/[locale]/(protected)/hr/documents/new/page.tsx` | 91 | `// TODO: 백엔드 API 구현 필요` (인사서류 신규등록) |
|
||||
|
||||
#### 생산 (Production) - 2건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/components/production/WorkOrders/WipProductionModal.tsx` | 106 | `// TODO: API 연동` (WIP 생산 모달) |
|
||||
| `src/app/[locale]/(protected)/quality/qms/components/InspectionModal.tsx` | 340 | `// TODO: 실제 저장 API 연동` (품질검사 저장) |
|
||||
|
||||
#### 게시판 (Board) - 2건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/components/board/BoardDetail/index.tsx` | 85 | `// TODO: 댓글 API 연동 (별도 작업)` |
|
||||
| `src/app/[locale]/(protected)/board/[boardCode]/[postId]/page.tsx` | 45 | `// TODO: 댓글 API 호출 추가` |
|
||||
|
||||
#### 설정 (Settings) - 1건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/components/settings/AttendanceSettingsManagement/index.tsx` | 18 | `// TODO: API 연동 시 작업 사항 - GET/PUT /api/settings/attendance` |
|
||||
|
||||
---
|
||||
|
||||
## B. 백엔드 필드 추가 대기 (10건)
|
||||
|
||||
### 모듈별 분류
|
||||
|
||||
#### 대시보드 트랜스포머 - 3건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/lib/api/dashboard/transformers.ts` | 261 | `// TODO: 백엔드 daily_change 필드 제공 시 더미값 제거` |
|
||||
| `src/lib/api/dashboard/transformers.ts` | 427 | `// TODO: 백엔드 per-card sub_label/count 제공 시 더미값 제거` |
|
||||
| `src/lib/api/dashboard/transformers.ts` | 654 | `// TODO: 백엔드 sub_label 필드 제공 시 더미값 제거` |
|
||||
|
||||
#### 공정관리 (Process Management) - 3건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/components/process-management/actions.ts` | 486 | `// TODO: API 응답에 process_name, process_category 필드 추가 후 활성화` |
|
||||
| `src/components/process-management/RuleModal.tsx` | 285 | `// TODO: API에서 process_name, process_category 필드 지원 후 실제 데이터 표시` |
|
||||
| `src/components/process-management/RuleModal.tsx` | 322 | `// TODO: API 지원 후 item.processName / item.processCategory 표시` |
|
||||
|
||||
#### 생산 (Production) - 2건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/components/production/WorkOrders/WorkOrderList.tsx` | 181 | `// TODO: API에서 긴급 건수 제공 시 연동` |
|
||||
| `src/components/production/WorkOrders/WorkOrderList.tsx` | 187 | `// TODO: API에서 지연 건수 제공 시 연동` |
|
||||
|
||||
#### 게시판 (Board) - 2건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/components/board/BoardManagement/BoardForm.tsx` | 24 | `// TODO: API에서 부서 목록 가져오기` |
|
||||
| `src/components/board/BoardManagement/BoardForm.tsx` | 33 | `// TODO: API에서 권한 목록 가져오기` |
|
||||
|
||||
---
|
||||
|
||||
## C. UI/기능 구현 대기 (16건)
|
||||
|
||||
### 모듈별 분류
|
||||
|
||||
#### 다운로드/출력 기능 - 4건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/components/accounting/BadDebtCollection/BadDebtDetail.tsx` | 268 | `// TODO: 실제 다운로드 로직` |
|
||||
| `src/app/[locale]/(protected)/quality/qms/components/Day1DocumentSection.tsx` | 149 | `// TODO: 다운로드 기능` |
|
||||
| `src/app/[locale]/(protected)/quality/qms/components/InspectionModal.tsx` | 472 | `// 다운로드 핸들러 (TODO: 실제 구현)` |
|
||||
| `src/components/material/ReceivingManagement/ReceivingReceiptDialog.tsx` | 23 | `// TODO: PDF 다운로드 기능` |
|
||||
|
||||
#### 품목 (Items) - 4건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/components/items/ItemForm/BOMSection.tsx` | 115 | `// TODO: 실제 itemMasters 데이터로 교체 필요` |
|
||||
| `src/components/items/ItemForm/BOMSection.tsx` | 196 | `// TODO: 품목 선택 시 데이터 채우기 로직` |
|
||||
| `src/components/items/ItemForm/BOMSection.tsx` | 209 | `// TODO: pricing에서 가져오기` (unitPrice) |
|
||||
| `src/components/items/ItemForm/hooks/useBOMManagement.ts` | 89 | `// TODO: pricing에서 가져오기` (unitPrice) |
|
||||
|
||||
#### 생산 (Production) - 3건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/components/production/WorkOrders/WorkOrderEdit.tsx` | 277 | `// TODO: API 호출로 서버에 상태 저장` |
|
||||
| `src/components/production/WorkOrders/WorkOrderEdit.tsx` | 310 | `// TODO: API 호출로 서버에 저장` |
|
||||
| `src/components/production/WorkOrders/WorkOrderEdit.tsx` | 327 | `// TODO: API 호출로 서버에서 삭제` |
|
||||
|
||||
#### 기타 기능 - 5건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/components/approval/DocumentDetail/index.tsx` | 161 | `// TODO: 공유 기능 추가 예정 - PDF 다운로드, 이메일, 팩스, 카카오톡 공유` |
|
||||
| `src/components/pricing/PricingListClient.tsx` | 177 | `// TODO: 이력 다이얼로그 열기` |
|
||||
| `src/components/pricing/PricingListClient.tsx` | 335 | `// TODO: API 연동 시 품목 마스터 동기화 로직 구현` |
|
||||
| `src/components/hr/SalaryManagement/index.tsx` | 253 | `// TODO: 지급항목 추가 다이얼로그 또는 로직 구현` |
|
||||
| `src/components/production/WorkResults/WorkResultList.tsx` | 70 | `// TODO: 상세 보기 기능 구현` |
|
||||
|
||||
---
|
||||
|
||||
## D. Phase 2 / 장기 과제 (10건)
|
||||
|
||||
### 모듈별 분류
|
||||
|
||||
#### 품목코드 생성 로직 개선 - 3건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/components/items/DynamicItemForm/utils/itemCodeGenerator.ts` | 11 | `// TODO: 추후 백엔드 API 또는 품목기준관리에서 설정 가능하도록 변경` |
|
||||
| `src/components/items/DynamicItemForm/utils/itemCodeGenerator.ts` | 52 | `// TODO: 추후 품목기준관리에서 설정 가능하도록 변경` |
|
||||
| `src/components/items/DynamicItemForm/utils/itemCodeGenerator.ts` | 98 | `// TODO: 추후 품목기준관리에서 설정 가능하도록 변경` |
|
||||
|
||||
#### 품목기준관리 하드코딩 대체 - 2건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/components/items/ItemMasterDataManagement/hooks/useTabManagement.ts` | 104 | `// TODO: 나중에 백엔드에서 기준값 로드로 대체 예정` |
|
||||
| `src/components/items/ItemMasterDataManagement/hooks/useAttributeManagement.ts` | 80 | `// TODO: 나중에 백엔드 API로 대체` (속성 옵션 하드코딩) |
|
||||
|
||||
#### 프로덕션 배포 준비 - 2건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/lib/api/toast-utils.ts` | 14 | `// TODO: 프로덕션 배포 시 false로 변경하거나 환경변수 사용` (SHOW_ERROR_CODE) |
|
||||
| `src/lib/api/error-handler.ts` | 112 | `// TODO: 프로덕션 배포 시 false로 변경하거나 환경변수 사용` (SHOW_ERROR_CODE) |
|
||||
|
||||
#### Phase 2 명시 - 2건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/lib/api/item-master.ts` | 99 | `// TODO: Phase 2에서 구현` |
|
||||
| `src/components/document-system/viewer/DocumentViewer.tsx` | 314 | `// TODO: BlockRenderer 구현 시 연결` (Phase 2 블록 렌더링) |
|
||||
|
||||
#### 기타 장기 - 1건
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/lib/api/items.ts` | 26 | `// TODO: 실제 인증 구현에 맞게 수정 필요` (getAuthToken) |
|
||||
|
||||
---
|
||||
|
||||
## E. CEO 대시보드 목업 데이터 (4건)
|
||||
|
||||
| 파일 | 라인 | TODO 내용 |
|
||||
|------|------|----------|
|
||||
| `src/components/business/CEODashboard/mockData.ts` | 5 | `// TODO: API 연동 시 이 파일을 API 호출로 대체` |
|
||||
| `src/components/business/CEODashboard/CEODashboard.tsx` | 253 | `// TODO: API 호출하여 일정 저장` |
|
||||
| `src/components/business/CEODashboard/CEODashboard.tsx` | 260 | `// TODO: API 호출하여 일정 삭제` |
|
||||
| `src/components/business/CEODashboard/sections/TodayIssueSection.tsx` | 411 | `// TODO: 버튼 - API 구현 후 활성화` (승인/반려 버튼) |
|
||||
|
||||
---
|
||||
|
||||
## F. 기타 (7건)
|
||||
|
||||
| 파일 | 라인 | TODO 내용 | 비고 |
|
||||
|------|------|----------|------|
|
||||
| `src/contexts/ItemMasterContext.tsx` | 1894 | `// TODO: 전체 init 데이터 새로고침 기능 구현 필요` | 리팩토링 |
|
||||
| `src/components/document-system/configs/index.ts` | 10 | `// TODO: Orders Configs` | 설정 추가 대기 |
|
||||
| `src/components/quotes/types.ts` | 737 | `// TODO: 동적으로 결정` (productCategory) | 로직 개선 |
|
||||
| `src/components/quotes/types.ts` | 875 | `// TODO: 동적으로 결정` (product_category) | 로직 개선 |
|
||||
| `src/components/production/WorkOrders/types.ts` | 516 | `// TODO: 실제 단계 추적 필요` (in_progress 상태) | 로직 개선 |
|
||||
| `src/components/items/ItemListClient.tsx` | 328 | `// TODO: 실제 API 호출로 데이터 저장` | API 연동 겸 로직 |
|
||||
| `src/components/business/construction/management/ProjectListClient.tsx` | 127 | `// TODO: 실제 API 연동 시 new Date()로 변경` (목업 데이터 임시 설정) | 목업 제거 |
|
||||
| `src/components/accounting/BadDebtCollection/BadDebtDetail.tsx` | 53 | `// TODO: API에서 조회` (담당자 목록 하드코딩) | API 연동 겸 데이터 |
|
||||
| `src/components/settings/CompanyInfoManagement/AddCompanyDialog.tsx` | 60 | `// TODO: 바로빌 API 연동` (외부 API) | 외부 서비스 연동 |
|
||||
| `src/components/accounting/SalesManagement/SalesDetail.tsx` | 500 | `// TODO: 거래명세서 조회 기능 연결` | 기능 연결 |
|
||||
| `src/app/[locale]/(protected)/hr/employee-management/csv-upload/page.tsx` | 8 | `// TODO: API 연동` (CSV 업로드) | API 연동 |
|
||||
| `src/app/[locale]/(protected)/hr/attendance/page.tsx` | 67 | `// TODO: 주소/좌표 설정 UI 추가 후 아래 주석 해제` | UI 추가 후 |
|
||||
| `src/components/business/construction/estimates/sections/EstimateDetailTableSection.tsx` | 224 | `// TODO: 견적 상세 기획서 수정 후 초기화 버튼 및 테이블 항목/데이터 재작업 필요` | 기획 대기 |
|
||||
| `src/components/material/ReceivingManagement/InspectionCreate.tsx` | 164 | `// TODO: API 호출` (검사 생성) | API 연동 |
|
||||
| `src/components/process-management/actions.ts` | 503 | `// TODO: 백엔드 API 수정 요청 - process_name, process_category 필드 추가` | 백엔드 요청 문서 |
|
||||
|
||||
> 참고: F 카테고리는 A~E에 명확히 분류되지 않는 항목을 포함합니다. 일부는 API 연동과 겹치지만 외부 서비스, 기획 대기, 로직 개선 등 복합적인 성격을 가집니다.
|
||||
|
||||
---
|
||||
|
||||
## 백엔드 팀 전달 요약 (A + B = 65건)
|
||||
|
||||
### 우선순위별 정리
|
||||
|
||||
#### 즉시 필요 (Critical) - 완전 Mock 상태
|
||||
|
||||
아래 모듈은 **전체 API가 Mock/로컬로 구현**되어 있어 백엔드 API 완성 즉시 교체 필요:
|
||||
|
||||
| 모듈 | 건수 | actions.ts 파일 |
|
||||
|------|------|-----------------|
|
||||
| 상품권 관리 | 6 | `accounting/GiftCertificateManagement/actions.ts` |
|
||||
| 세금계산서 관리 | 5 | `accounting/TaxInvoiceManagement/actions.ts` |
|
||||
| 세금계산서 발행 | 6 | `accounting/TaxInvoiceIssuance/actions.ts` |
|
||||
| 기성관리 | 5+3 | `construction/progress-billing/actions.ts` + hooks |
|
||||
| 프로젝트관리 | 1 | `construction/management/actions.ts` (전체 Mock) |
|
||||
| 품목기준관리 스토어 | 16 | `stores/item-master/useItemMasterStore.ts` |
|
||||
|
||||
**소계: 42건** - 이 모듈들은 프론트엔드 UI는 완성되었으나 백엔드 API 미구현 상태
|
||||
|
||||
#### 필드 추가 요청 (Important)
|
||||
|
||||
| 요청 대상 | 추가 필드 | 관련 파일 |
|
||||
|-----------|----------|----------|
|
||||
| CEO 대시보드 API | `daily_change`, `sub_label`, `count` | `lib/api/dashboard/transformers.ts` |
|
||||
| 공정관리 품목 API | `process_name`, `process_category` | `process-management/actions.ts`, `RuleModal.tsx` |
|
||||
| 작업지시 API | 긴급 건수, 지연 건수 | `WorkOrders/WorkOrderList.tsx` |
|
||||
| 게시판 관리 API | 부서 목록, 권한 목록 | `board/BoardManagement/BoardForm.tsx` |
|
||||
|
||||
**소계: 10건**
|
||||
|
||||
#### 신규 API 필요 (Normal)
|
||||
|
||||
| API | 용도 | 관련 파일 |
|
||||
|-----|------|----------|
|
||||
| 인사서류함 CRUD | HR 문서 관리 | `hr/documents/page.tsx`, `new/page.tsx` |
|
||||
| 댓글 CRUD | 게시판 댓글 | `BoardDetail/index.tsx`, `[postId]/page.tsx` |
|
||||
| 출퇴근 설정 | GET/PUT | `AttendanceSettingsManagement/index.tsx` |
|
||||
| 건설 현장관리 저장 | 현장/구조검토 | `SiteDetailForm.tsx`, `StructureReviewDetailForm.tsx` |
|
||||
| WIP 생산 | 생산 모달 | `WipProductionModal.tsx` |
|
||||
| 품질검사 저장 | 검사 결과 | `InspectionModal.tsx` |
|
||||
|
||||
**소계: 13건**
|
||||
|
||||
---
|
||||
|
||||
## 통계 요약
|
||||
|
||||
```
|
||||
전체 TODO: 102건
|
||||
├── 백엔드 의존: 65건 (63.7%) ← A + B
|
||||
├── 프론트 백로그: 16건 (15.7%) ← C
|
||||
├── 장기 과제: 10건 (9.8%) ← D
|
||||
├── 대시보드 목업: 4건 (3.9%) ← E
|
||||
└── 기타: 7건 (6.9%) ← F
|
||||
|
||||
프론트 독립 해결 가능: 26건 (C + D 일부)
|
||||
백엔드 완성 필요: 69건 (A + B + E)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **주의사항**
|
||||
> - `src/app/[locale]/(protected)/dev/dashboard/_components/AIPoweredDashboard.tsx`의 `TODO_ITEMS`는 변수명이며 실제 TODO 주석이 아니므로 제외
|
||||
> - 일부 항목은 복합 성격(API 연동 + UI 구현)을 가지며, 주된 블로커 기준으로 분류
|
||||
> - 이 문서는 2026-02-19 기준 스냅샷이며, 코드 변경에 따라 갱신 필요
|
||||
@@ -1,6 +1,6 @@
|
||||
# 전체 페이지 테스트 URL 목록
|
||||
|
||||
> 백엔드 메뉴 연동 전 테스트용 직접 접근 URL (Last Updated: 2025-12-19)
|
||||
> 백엔드 메뉴 연동 전 테스트용 직접 접근 URL (Last Updated: 2026-01-28)
|
||||
|
||||
## 🚀 클릭 가능한 웹 페이지
|
||||
|
||||
@@ -34,6 +34,7 @@ http://localhost:3000/ko/dashboard
|
||||
|--------|-----|------|
|
||||
| 부서관리 | `/ko/hr/department-management` | ✅ |
|
||||
| 사원관리 | `/ko/hr/employee-management` | ✅ |
|
||||
| **근태현황** | `/ko/hr/attendance-status` | ✅ |
|
||||
| 근태관리 | `/ko/hr/attendance-management` | ✅ |
|
||||
| 휴가관리 | `/ko/hr/vacation-management` | ✅ |
|
||||
| 급여관리 | `/ko/hr/salary-management` | ✅ |
|
||||
@@ -42,6 +43,7 @@ http://localhost:3000/ko/dashboard
|
||||
```
|
||||
http://localhost:3000/ko/hr/department-management
|
||||
http://localhost:3000/ko/hr/employee-management
|
||||
http://localhost:3000/ko/hr/attendance-status # 근태현황
|
||||
http://localhost:3000/ko/hr/attendance-management
|
||||
http://localhost:3000/ko/hr/vacation-management
|
||||
http://localhost:3000/ko/hr/salary-management
|
||||
@@ -56,12 +58,26 @@ http://localhost:3000/ko/hr/attendance # 🧪 모바일 출퇴근 (테스트)
|
||||
|--------|-----|------|
|
||||
| 거래처관리 | `/ko/sales/client-management-sales-admin` | ✅ |
|
||||
| 견적관리 | `/ko/sales/quote-management` | ✅ |
|
||||
| **수주관리** | `/ko/sales/order-management-sales` | ✅ |
|
||||
| 단가관리 | `/ko/sales/pricing-management` | ✅ |
|
||||
|
||||
### 견적 V2 테스트 (새 UI)
|
||||
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| **견적 등록 (V2)** | `/ko/sales/quote-management/test-new` | 🧪 테스트 |
|
||||
| **견적 상세 (V2)** | `/ko/sales/quote-management/test/1` | 🧪 테스트 |
|
||||
| **견적 수정 (V2)** | `/ko/sales/quote-management/test/1/edit` | 🧪 테스트 |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/sales/client-management-sales-admin
|
||||
http://localhost:3000/ko/sales/quote-management
|
||||
http://localhost:3000/ko/sales/pricing-management
|
||||
|
||||
# 견적 V2 테스트 (새 UI)
|
||||
http://localhost:3000/ko/sales/quote-management/test-new # 🧪 견적 등록 V2
|
||||
http://localhost:3000/ko/sales/quote-management/test/1 # 🧪 견적 상세 V2
|
||||
http://localhost:3000/ko/sales/quote-management/test/1/edit # 🧪 견적 수정 V2
|
||||
```
|
||||
|
||||
---
|
||||
@@ -71,9 +87,17 @@ http://localhost:3000/ko/sales/pricing-management
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| 품목기준관리 | `/ko/master-data/item-master-data-management` | ✅ |
|
||||
| **공정관리** | `/ko/master-data/process-management` | ✅ |
|
||||
| **단가표관리** | `/ko/master-data/pricing-table-management` | 🆕 NEW |
|
||||
| **└ 단가배포관리** | `/ko/master-data/price-distribution` | 🆕 NEW |
|
||||
| **점검표관리** | `/ko/master-data/checklist-management` | 🆕 NEW |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/master-data/item-master-data-management
|
||||
http://localhost:3000/ko/master-data/process-management # 공정관리
|
||||
http://localhost:3000/ko/master-data/pricing-table-management # 🆕 단가표관리
|
||||
http://localhost:3000/ko/master-data/price-distribution # 🆕 단가배포관리
|
||||
http://localhost:3000/ko/master-data/checklist-management # 🆕 점검표관리
|
||||
```
|
||||
|
||||
---
|
||||
@@ -83,9 +107,71 @@ http://localhost:3000/ko/master-data/item-master-data-management
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| 스크린 생산 | `/ko/production/screen-production` | ✅ |
|
||||
| 작업지시 관리 | `/ko/production/work-orders` | ✅ |
|
||||
| **작업실적 조회** | `/ko/production/work-results` | 🆕 NEW |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/production/screen-production
|
||||
http://localhost:3000/ko/production/work-orders
|
||||
http://localhost:3000/ko/production/work-results # 🆕 작업실적 조회
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 자재관리 (Material)
|
||||
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| **재고현황** | `/ko/material/stock-status` | 🆕 NEW |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/material/stock-status # 🆕 재고현황
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔬 품질관리 (Quality)
|
||||
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| **검사관리** | `/ko/quality/inspections` | 🆕 NEW |
|
||||
| **실적신고관리** | `/ko/quality/performance-reports` | 🆕 NEW |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/quality/inspections # 🆕 검사관리
|
||||
http://localhost:3000/ko/quality/performance-reports # 🆕 실적신고관리
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚗 차량/지게차 (Vehicle Management)
|
||||
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| **차량관리** | `/ko/vehicle-management/vehicle` | 🆕 NEW |
|
||||
| **차량일지/월간사진기록** | `/ko/vehicle-management/vehicle-log` | 🆕 NEW |
|
||||
| **지게차 관리** | `/ko/vehicle-management/forklift` | 🆕 NEW |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/vehicle-management/vehicle # 🆕 차량관리
|
||||
http://localhost:3000/ko/vehicle-management/vehicle-log # 🆕 차량일지/월간사진기록
|
||||
http://localhost:3000/ko/vehicle-management/forklift # 🆕 지게차 관리
|
||||
```
|
||||
|
||||
> ℹ️ **참고**: 각 페이지에서 등록/상세/수정 페이지로 이동 가능 (별도 URL 등록 불필요)
|
||||
|
||||
---
|
||||
|
||||
## 📤 출고관리 (Outbound)
|
||||
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| **출하 목록** | `/ko/outbound/shipments` | 🆕 NEW |
|
||||
| **배차차량 목록** | `/ko/outbound/vehicle-dispatches` | 🆕 NEW |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/outbound/shipments # 🆕 출하관리
|
||||
http://localhost:3000/ko/outbound/vehicle-dispatches # 🆕 배차차량관리
|
||||
```
|
||||
|
||||
---
|
||||
@@ -102,9 +188,11 @@ http://localhost:3000/ko/production/screen-production
|
||||
| **출퇴근관리** | `/ko/settings/attendance-settings` | ✅ |
|
||||
| **계좌관리** | `/ko/settings/accounts` | ✅ |
|
||||
| **카드관리** | `/ko/hr/card-management` | 🆕 NEW |
|
||||
| **달력관리** | `/ko/settings/calendar-management` | 🆕 NEW |
|
||||
| **게시판관리** | `/ko/board/board-management` | 🆕 NEW |
|
||||
| **팝업관리** | `/ko/settings/popup-management` | 🆕 NEW |
|
||||
| **알림설정** | `/ko/settings/notification-settings` | 🆕 NEW |
|
||||
| **바로빌연동관리** | `/ko/settings/barobill-integration` | 🆕 NEW |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/settings/leave-policy
|
||||
@@ -116,8 +204,10 @@ http://localhost:3000/ko/settings/attendance-settings # 출퇴근관리
|
||||
http://localhost:3000/ko/settings/accounts # 계좌관리
|
||||
http://localhost:3000/ko/settings/notification-settings # 🆕 알림설정
|
||||
http://localhost:3000/ko/hr/card-management # 🆕 카드관리
|
||||
http://localhost:3000/ko/settings/calendar-management # 🆕 달력관리
|
||||
http://localhost:3000/ko/board/board-management # 🆕 게시판관리
|
||||
http://localhost:3000/ko/settings/popup-management # 🆕 팝업관리
|
||||
http://localhost:3000/ko/settings/barobill-integration # 🆕 바로빌연동관리
|
||||
```
|
||||
|
||||
---
|
||||
@@ -155,6 +245,10 @@ http://localhost:3000/ko/approval/reference # ✅ 참조함
|
||||
| **입출금 계좌조회** | `/ko/accounting/bank-transactions` | ✅ |
|
||||
| **카드 내역 조회** | `/ko/accounting/card-transactions` | 🆕 NEW |
|
||||
| **악성채권 추심관리** | `/ko/accounting/bad-debt-collection` | 🆕 NEW |
|
||||
| **세금계산서 발행** | `/ko/accounting/tax-invoice-issuance` | 🆕 NEW |
|
||||
| **세금계산서 관리** | `/ko/accounting/tax-invoices` | 🆕 NEW |
|
||||
| **상품권관리** | `/ko/accounting/gift-certificates` | 🆕 NEW |
|
||||
| **일반전표입력** | `/ko/accounting/general-journal-entry` | 🆕 NEW |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/accounting/vendors # 거래처관리
|
||||
@@ -170,6 +264,10 @@ http://localhost:3000/ko/accounting/receivables-status # 미수금 현황
|
||||
http://localhost:3000/ko/accounting/bank-transactions # 입출금 계좌조회
|
||||
http://localhost:3000/ko/accounting/card-transactions # 카드 내역 조회
|
||||
http://localhost:3000/ko/accounting/bad-debt-collection # 악성채권 추심관리
|
||||
http://localhost:3000/ko/accounting/tax-invoice-issuance # 🆕 세금계산서 발행
|
||||
http://localhost:3000/ko/accounting/tax-invoices # 🆕 세금계산서 관리
|
||||
http://localhost:3000/ko/accounting/gift-certificates # 🆕 상품권관리
|
||||
http://localhost:3000/ko/accounting/general-journal-entry # 🆕 일반전표입력
|
||||
```
|
||||
|
||||
---
|
||||
@@ -179,9 +277,11 @@ http://localhost:3000/ko/accounting/bad-debt-collection # 악성채권 추심
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| **게시판 목록** | `/ko/board` | ✅ |
|
||||
| **게시판 상세** | `/ko/boards/[boardCode]` | ✅ |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/board # 게시판 목록
|
||||
http://localhost:3000/ko/boards/notice # 게시판 상세 (예: 공지사항)
|
||||
```
|
||||
|
||||
> ⚠️ **참고**: 게시판관리는 설정(Settings)에서 관리합니다
|
||||
@@ -209,13 +309,13 @@ http://localhost:3000/ko/reports/comprehensive-analysis # 종합 경영 분석
|
||||
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| **계정정보** | `/ko/account-info` | 🆕 NEW |
|
||||
| **계정정보** | `/ko/settings/account-info` | 🆕 NEW |
|
||||
| **회사정보** | `/ko/company-info` | 🆕 NEW |
|
||||
| **구독관리** | `/ko/subscription` | 🆕 NEW |
|
||||
| **결제내역** | `/ko/payment-history` | 🆕 NEW |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/account-info # 계정정보
|
||||
http://localhost:3000/ko/settings/account-info # 계정정보
|
||||
http://localhost:3000/ko/company-info # 회사정보
|
||||
http://localhost:3000/ko/subscription # 구독관리
|
||||
http://localhost:3000/ko/payment-history # 결제내역
|
||||
@@ -233,13 +333,13 @@ http://localhost:3000/ko/payment-history # 결제내역
|
||||
| **공지사항** | `/ko/customer-center/notices` | ✅ |
|
||||
| **이벤트** | `/ko/customer-center/events` | ✅ |
|
||||
| **FAQ** | `/ko/customer-center/faq` | 🆕 NEW |
|
||||
| **1:1 문의** | `/ko/customer-center/inquiries` | 🆕 NEW |
|
||||
| **1:1 문의** | `/ko/customer-center/qna` | ✅ |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/customer-center/notices # 공지사항
|
||||
http://localhost:3000/ko/customer-center/events # 이벤트
|
||||
http://localhost:3000/ko/customer-center/faq # FAQ
|
||||
http://localhost:3000/ko/customer-center/inquiries # 1:1 문의
|
||||
http://localhost:3000/ko/customer-center/qna # 1:1 문의
|
||||
```
|
||||
|
||||
> ℹ️ **고객센터 메뉴**: 공지사항, 이벤트, FAQ, 1:1 문의
|
||||
@@ -273,11 +373,39 @@ http://localhost:3000/ko/sales/pricing-management
|
||||
### Master Data
|
||||
```
|
||||
http://localhost:3000/ko/master-data/item-master-data-management
|
||||
http://localhost:3000/ko/master-data/pricing-table-management # 🆕 단가표관리
|
||||
http://localhost:3000/ko/master-data/price-distribution # 🆕 단가배포관리
|
||||
```
|
||||
|
||||
### Production
|
||||
```
|
||||
http://localhost:3000/ko/production/screen-production
|
||||
http://localhost:3000/ko/production/work-orders
|
||||
http://localhost:3000/ko/production/work-results # 🆕 작업실적 조회
|
||||
```
|
||||
|
||||
### Material
|
||||
```
|
||||
http://localhost:3000/ko/material/stock-status # 🆕 재고현황
|
||||
```
|
||||
|
||||
### Quality
|
||||
```
|
||||
http://localhost:3000/ko/quality/inspections # 🆕 검사관리
|
||||
http://localhost:3000/ko/quality/performance-reports # 🆕 실적신고관리
|
||||
```
|
||||
|
||||
### Outbound
|
||||
```
|
||||
http://localhost:3000/ko/outbound/shipments # 🆕 출하관리
|
||||
http://localhost:3000/ko/outbound/vehicle-dispatches # 🆕 배차차량관리
|
||||
```
|
||||
|
||||
### Vehicle Management (차량/지게차)
|
||||
```
|
||||
http://localhost:3000/ko/vehicle-management/vehicle # 🆕 차량관리
|
||||
http://localhost:3000/ko/vehicle-management/vehicle-log # 🆕 차량일지/월간사진기록
|
||||
http://localhost:3000/ko/vehicle-management/forklift # 🆕 지게차 관리
|
||||
```
|
||||
|
||||
### Settings
|
||||
@@ -291,8 +419,10 @@ http://localhost:3000/ko/settings/attendance-settings # 출퇴근관리
|
||||
http://localhost:3000/ko/settings/accounts # 계좌관리
|
||||
http://localhost:3000/ko/settings/notification-settings # 🆕 알림설정
|
||||
http://localhost:3000/ko/hr/card-management # 🆕 카드관리
|
||||
http://localhost:3000/ko/settings/calendar-management # 🆕 달력관리
|
||||
http://localhost:3000/ko/board/board-management # 🆕 게시판관리
|
||||
http://localhost:3000/ko/settings/popup-management # 🆕 팝업관리
|
||||
http://localhost:3000/ko/settings/barobill-integration # 🆕 바로빌연동관리
|
||||
```
|
||||
|
||||
### Approval
|
||||
@@ -317,6 +447,9 @@ http://localhost:3000/ko/accounting/receivables-status # 미수금 현황
|
||||
http://localhost:3000/ko/accounting/bank-transactions # 입출금 계좌조회
|
||||
http://localhost:3000/ko/accounting/card-transactions # 🆕 카드 내역 조회
|
||||
http://localhost:3000/ko/accounting/bad-debt-collection # 악성채권 추심관리
|
||||
http://localhost:3000/ko/accounting/tax-invoice-issuance # 🆕 세금계산서 발행
|
||||
http://localhost:3000/ko/accounting/gift-certificates # 🆕 상품권관리
|
||||
http://localhost:3000/ko/accounting/general-journal-entry # 🆕 일반전표입력
|
||||
```
|
||||
|
||||
### Board
|
||||
@@ -334,7 +467,23 @@ http://localhost:3000/ko/reports/comprehensive-analysis # 종합 경영 분석
|
||||
http://localhost:3000/ko/customer-center/notices # 공지사항
|
||||
http://localhost:3000/ko/customer-center/events # 이벤트
|
||||
http://localhost:3000/ko/customer-center/faq # FAQ
|
||||
http://localhost:3000/ko/customer-center/inquiries # 1:1 문의
|
||||
http://localhost:3000/ko/customer-center/qna # 1:1 문의
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 개발/테스트 (Dev)
|
||||
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| **테스트 URL 목록** | `/ko/dev/test-urls` | ✅ |
|
||||
| **기업 신용분석 모달 테스트** | `/ko/dev/credit-analysis-test` | 🧪 테스트 |
|
||||
| **Editable Table 테스트** | `/ko/dev/editable-table` | 🧪 테스트 |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/dev/test-urls # 테스트 URL 목록
|
||||
http://localhost:3000/ko/dev/credit-analysis-test # 기업 신용분석 모달 테스트
|
||||
http://localhost:3000/ko/dev/editable-table # Editable Table 테스트
|
||||
```
|
||||
|
||||
---
|
||||
@@ -356,9 +505,29 @@ http://localhost:3000/ko/customer-center/inquiries # 1:1 문의
|
||||
|
||||
// Master Data
|
||||
'/master-data/item-master-data-management'
|
||||
'/master-data/pricing-table-management' // 단가표관리 (🆕 NEW)
|
||||
'/master-data/price-distribution' // 단가배포관리 (🆕 NEW)
|
||||
|
||||
// Production
|
||||
'/production/screen-production'
|
||||
'/production/work-orders' // 작업지시 관리
|
||||
'/production/work-results' // 작업실적 조회 (🆕 NEW)
|
||||
|
||||
// Material (자재관리)
|
||||
'/material/stock-status' // 재고현황 (🆕 NEW)
|
||||
|
||||
// Quality (품질관리)
|
||||
'/quality/inspections' // 검사관리 (🆕 NEW)
|
||||
'/quality/performance-reports' // 실적신고관리 (🆕 NEW)
|
||||
|
||||
// Outbound (출고관리)
|
||||
'/outbound/shipments' // 출하관리 (🆕 NEW)
|
||||
'/outbound/vehicle-dispatches' // 배차차량관리 (🆕 NEW)
|
||||
|
||||
// Vehicle Management (차량/지게차)
|
||||
'/vehicle-management/vehicle' // 차량관리 (🆕 NEW)
|
||||
'/vehicle-management/vehicle-log' // 차량일지/월간사진기록 (🆕 NEW)
|
||||
'/vehicle-management/forklift' // 지게차 관리 (🆕 NEW)
|
||||
|
||||
// Settings
|
||||
'/settings/leave-policy'
|
||||
@@ -370,11 +539,13 @@ http://localhost:3000/ko/customer-center/inquiries # 1:1 문의
|
||||
'/settings/accounts' // 계좌관리
|
||||
'/settings/notification-settings' // 알림설정 (🆕 NEW)
|
||||
'/hr/card-management' // 카드관리 (🆕 NEW)
|
||||
'/settings/calendar-management' // 달력관리 (🆕 NEW)
|
||||
'/board/board-management' // 게시판관리 (🆕 NEW)
|
||||
'/settings/popup-management' // 팝업관리 (🆕 NEW)
|
||||
'/settings/barobill-integration' // 바로빌연동관리 (🆕 NEW)
|
||||
|
||||
// 계정/회사/구독 (사이드바 루트 레벨 별도 메뉴)
|
||||
'/account-info' // 계정정보 (🆕 NEW)
|
||||
'/settings/account-info' // 계정정보 (🆕 NEW)
|
||||
'/company-info' // 회사정보 (🆕 NEW)
|
||||
'/subscription' // 구독관리 (🆕 NEW)
|
||||
'/payment-history' // 결제내역 (🆕 NEW)
|
||||
@@ -398,6 +569,9 @@ http://localhost:3000/ko/customer-center/inquiries # 1:1 문의
|
||||
'/accounting/bank-transactions' // 입출금 계좌조회
|
||||
'/accounting/card-transactions' // 카드 내역 조회
|
||||
'/accounting/bad-debt-collection' // 악성채권 추심관리
|
||||
'/accounting/tax-invoice-issuance' // 세금계산서 발행 (🆕 NEW)
|
||||
'/accounting/gift-certificates' // 상품권관리 (🆕 NEW)
|
||||
'/accounting/general-journal-entry' // 일반전표입력 (🆕 NEW)
|
||||
|
||||
// Board (게시판)
|
||||
'/board' // 게시판 목록
|
||||
@@ -409,7 +583,7 @@ http://localhost:3000/ko/customer-center/inquiries # 1:1 문의
|
||||
'/customer-center/notices' // 공지사항
|
||||
'/customer-center/events' // 이벤트
|
||||
'/customer-center/faq' // FAQ (🆕 NEW)
|
||||
'/customer-center/inquiries' // 1:1 문의 (🆕 NEW)
|
||||
'/customer-center/qna' // 1:1 문의
|
||||
```
|
||||
|
||||
---
|
||||
@@ -417,4 +591,4 @@ http://localhost:3000/ko/customer-center/inquiries # 1:1 문의
|
||||
## 작성일
|
||||
|
||||
- 최초 작성: 2025-12-06
|
||||
- 최종 업데이트: 2025-12-19 (하위 페이지 정리, 리스트 페이지만 유지)
|
||||
- 최종 업데이트: 2026-02-13 (일반전표입력 추가)
|
||||
118
claudedocs/dev/[REF] chrome-devtools-mcp-emoji-issue.md
Normal file
118
claudedocs/dev/[REF] chrome-devtools-mcp-emoji-issue.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Chrome DevTools MCP - 이모지 JSON 직렬화 오류
|
||||
|
||||
> 작성일: 2025-01-17
|
||||
|
||||
## 문제 현상
|
||||
|
||||
Chrome DevTools MCP가 특정 페이지 접근 시 다운되는 현상
|
||||
|
||||
### 에러 메시지
|
||||
```
|
||||
API Error: 400 {"type":"error","error":{"type":"invalid_request_error",
|
||||
"message":"The request body is not valid JSON: invalid high surrogate in string:
|
||||
line 1 column XXXXX (char XXXXX)"},"request_id":"req_XXXXX"}
|
||||
```
|
||||
|
||||
### 발생 조건
|
||||
- 페이지에 **이모지**가 많이 포함된 경우
|
||||
- `take_snapshot` 또는 다른 MCP 도구 호출 시
|
||||
- a11y tree를 JSON으로 직렬화하는 과정에서 발생
|
||||
|
||||
## 원인
|
||||
|
||||
### 유니코드 서로게이트 쌍 (Surrogate Pair) 문제
|
||||
|
||||
이모지는 UTF-16에서 **서로게이트 쌍**으로 인코딩됨:
|
||||
- High surrogate: U+D800 ~ U+DBFF
|
||||
- Low surrogate: U+DC00 ~ U+DFFF
|
||||
|
||||
Chrome DevTools MCP가 페이지 스냅샷을 JSON으로 직렬화할 때, 이모지의 서로게이트 쌍이 깨지면서 "invalid high surrogate" 오류 발생.
|
||||
|
||||
### 문제가 되는 케이스
|
||||
1. **DOM에 직접 렌더링된 이모지**: `<span>🏠</span>`
|
||||
2. **데이터에 포함된 이모지**: API 응답, 파싱된 데이터
|
||||
3. **대량의 이모지**: 수십 개 이상의 이모지가 한 페이지에 존재
|
||||
|
||||
## 해결 방법
|
||||
|
||||
### 1. 이모지를 Lucide 아이콘으로 교체 (UI)
|
||||
|
||||
**Before**
|
||||
```tsx
|
||||
const iconMap = {
|
||||
'기본': '🏠',
|
||||
'인사관리': '👥',
|
||||
};
|
||||
|
||||
<span className="text-xl">{category.icon}</span>
|
||||
```
|
||||
|
||||
**After**
|
||||
```tsx
|
||||
import { Home, Users, type LucideIcon } from 'lucide-react';
|
||||
|
||||
const iconComponents: Record<string, LucideIcon> = {
|
||||
Home,
|
||||
Users,
|
||||
};
|
||||
|
||||
function CategoryIcon({ name }: { name: string }) {
|
||||
const IconComponent = iconComponents[name] || FileText;
|
||||
return <IconComponent className="w-5 h-5" />;
|
||||
}
|
||||
|
||||
<CategoryIcon name={category.icon} />
|
||||
```
|
||||
|
||||
### 2. 데이터 파싱 시 이모지 제거/변환 (Server)
|
||||
|
||||
```typescript
|
||||
function convertEmojiToText(text: string): string {
|
||||
// 특정 이모지를 의미있는 텍스트로 변환
|
||||
let result = text
|
||||
.replace(/✅/g, '[완료]')
|
||||
.replace(/⚠️?/g, '[주의]')
|
||||
.replace(/🧪/g, '[테스트]')
|
||||
.replace(/🆕/g, '[NEW]')
|
||||
.replace(/•/g, '-');
|
||||
|
||||
// 모든 이모지 및 특수 유니코드 문자 제거
|
||||
result = result
|
||||
.replace(/[\u{1F300}-\u{1F9FF}]/gu, '') // 이모지 범위
|
||||
.replace(/[\u{2600}-\u{26FF}]/gu, '') // 기타 기호
|
||||
.replace(/[\u{2700}-\u{27BF}]/gu, '') // 딩뱃
|
||||
.replace(/[\u{FE00}-\u{FE0F}]/gu, '') // Variation Selectors
|
||||
.replace(/[\u{1F000}-\u{1F02F}]/gu, '') // 마작 타일
|
||||
.replace(/[\u{1F0A0}-\u{1F0FF}]/gu, '') // 플레잉 카드
|
||||
.replace(/[\u200D]/g, '') // Zero Width Joiner
|
||||
.trim();
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
## 체크리스트
|
||||
|
||||
새 페이지 개발 시 Chrome DevTools MCP 호환성 확인:
|
||||
|
||||
- [ ] 페이지에 이모지 직접 렌더링하지 않음
|
||||
- [ ] 아이콘은 Lucide 또는 SVG 사용
|
||||
- [ ] 외부 데이터(API, 파일) 파싱 시 이모지 제거 처리
|
||||
- [ ] status, label 등에 이모지 대신 텍스트 사용
|
||||
|
||||
## 관련 파일
|
||||
|
||||
이 문제로 수정된 파일들:
|
||||
|
||||
| 파일 | 변경 내용 |
|
||||
|------|----------|
|
||||
| `dev/test-urls/actions.ts` | iconMap, convertEmojiToText 함수 추가 |
|
||||
| `dev/test-urls/TestUrlsClient.tsx` | Lucide 아이콘 동적 렌더링 |
|
||||
| `dev/construction-test-urls/actions.ts` | 동일 |
|
||||
| `dev/construction-test-urls/ConstructionTestUrlsClient.tsx` | 동일 |
|
||||
|
||||
## 참고
|
||||
|
||||
- 이 문제는 Chrome DevTools MCP의 JSON 직렬화 로직에서 발생
|
||||
- MCP 자체 버그일 가능성 있으나, 클라이언트에서 이모지 제거로 우회 가능
|
||||
- 다른 MCP 도구에서도 비슷한 문제 발생 가능성 있음
|
||||
52
claudedocs/dev/[REF] construction-pages-test-urls.md
Normal file
52
claudedocs/dev/[REF] construction-pages-test-urls.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Juil Enterprise Test URLs
|
||||
Last Updated: 2026-01-23
|
||||
|
||||
## 프로젝트 관리 (Project)
|
||||
|
||||
### 프로젝트관리 (Management)
|
||||
| 페이지 | URL | 상태 |
|
||||
|---|---|---|
|
||||
| **프로젝트 관리** | `/ko/construction/project/management` | ✅ 완료 |
|
||||
| **프로젝트실행관리** | `/ko/construction/project/execution-management` | ✅ 완료 |
|
||||
|
||||
### 입찰관리 (Bidding)
|
||||
| 페이지 | URL | 상태 |
|
||||
|---|---|---|
|
||||
| **거래처 관리** | `/ko/construction/project/bidding/partners` | ✅ 완료 |
|
||||
| **현장설명회관리** | `/ko/construction/project/bidding/site-briefings` | ✅ 완료 |
|
||||
| **견적관리** | `/ko/construction/project/bidding/estimates` | ✅ 완료 |
|
||||
| **입찰관리** | `/ko/construction/project/bidding` | ✅ 완료 |
|
||||
|
||||
### 계약관리 (Contract)
|
||||
| 페이지 | URL | 상태 |
|
||||
|---|---|---|
|
||||
| **계약관리** | `/ko/construction/project/contract` | 🆕 NEW |
|
||||
| **인수인계보고서관리** | `/ko/construction/project/contract/handover-report` | 🆕 NEW |
|
||||
|
||||
### 발주관리 (Order)
|
||||
| 페이지 | URL | 상태 |
|
||||
|---|---|---|
|
||||
| **현장관리** | `/ko/construction/order/site-management` | 🆕 NEW |
|
||||
| **구조검토관리** | `/ko/construction/order/structure-review` | 🆕 NEW |
|
||||
| **발주관리** | `/ko/construction/order/order-management` | 🆕 NEW |
|
||||
|
||||
### 공사관리 (Construction)
|
||||
| 페이지 | URL | 상태 |
|
||||
|---|---|---|
|
||||
| **시공관리** | `/ko/construction/project/construction-management` | ✅ 완료 |
|
||||
| **이슈관리** | `/ko/construction/project/issue-management` | ✅ 완료 |
|
||||
| **공과관리** | `/ko/construction/project/utility-management` | 🆕 NEW |
|
||||
| **작업인력현황** | `/ko/construction/project/worker-status` | ✅ 완료 |
|
||||
|
||||
### 기성청구관리 (Billing)
|
||||
| 페이지 | URL | 상태 |
|
||||
|---|---|---|
|
||||
| **기성청구관리** | `/ko/construction/billing/progress-billing-management` | 🆕 NEW |
|
||||
|
||||
### 기준정보 (Base Info) - 발주관리 하위
|
||||
| 페이지 | URL | 상태 |
|
||||
|---|---|---|
|
||||
| **카테고리관리** | `/ko/construction/order/base-info/categories` | 🆕 NEW |
|
||||
| **품목관리** | `/ko/construction/order/base-info/items` | 🆕 NEW |
|
||||
| **단가관리** | `/ko/construction/order/base-info/pricing` | 🆕 NEW |
|
||||
| **노임관리** | `/ko/construction/order/base-info/labor` | 🆕 NEW |
|
||||
298
claudedocs/dev/[REF] page-builder-implementation.md
Normal file
298
claudedocs/dev/[REF] page-builder-implementation.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# 페이지 빌더 (Page Builder) 구현 문서
|
||||
|
||||
> **작성일**: 2026-01-22
|
||||
> **상태**: 개발 중 (테스트 버전)
|
||||
> **경로**: `/dev/page-builder`
|
||||
> **Git 상태**: `.gitignore`에 등록되어 버전 관리 제외 (테스트용)
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요
|
||||
|
||||
### 1.1 목적
|
||||
품목기준관리(Item Master Data Management)의 폼 구조를 **시각적으로(WYSIWYG)** 편집할 수 있는 Framer 스타일의 페이지 빌더입니다.
|
||||
|
||||
### 1.2 핵심 기능
|
||||
- 드래그 앤 드롭으로 섹션/필드 배치
|
||||
- 실시간 미리보기 (데스크탑/태블릿/모바일)
|
||||
- **API 연동**: 품목기준관리 API와 동기화
|
||||
- 조건부 표시 설정 (필드 값에 따른 섹션/필드 표시/숨김)
|
||||
- Undo/Redo 지원
|
||||
- JSON 내보내기/가져오기
|
||||
|
||||
### 1.3 접근 방법
|
||||
```
|
||||
URL: http://localhost:3000/dev/page-builder
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 파일 구조
|
||||
|
||||
```
|
||||
src/app/[locale]/(protected)/dev/page-builder/
|
||||
├── page.tsx # 페이지 진입점
|
||||
├── PageBuilderClient.tsx # 메인 클라이언트 컴포넌트
|
||||
├── PLAN.md # 초기 기획 문서
|
||||
│
|
||||
├── components/
|
||||
│ ├── index.ts # 컴포넌트 배럴 export
|
||||
│ ├── ComponentPalette.tsx # 좌측 컴포넌트 팔레트 (섹션/필드 드래그)
|
||||
│ ├── BuilderCanvas.tsx # 중앙 캔버스 (드롭 영역, 미리보기)
|
||||
│ ├── PropertyPanel.tsx # 우측 속성 패널 (섹션/필드 편집)
|
||||
│ ├── PageSelector.tsx # 페이지 목록 관리
|
||||
│ └── ConditionEditor.tsx # 조건부 표시 설정 UI
|
||||
│
|
||||
├── hooks/
|
||||
│ ├── usePageBuilder.ts # 섹션/필드 CRUD, 선택 상태 관리
|
||||
│ ├── usePageManager.ts # 페이지 CRUD, API 연동, 저장/로드
|
||||
│ ├── useHistory.ts # Undo/Redo 히스토리 관리
|
||||
│ └── useItemMasterSync.ts # (미사용) 초기 API 동기화 시도
|
||||
│
|
||||
├── types/
|
||||
│ ├── index.ts # 타입 배럴 export
|
||||
│ ├── builder.types.ts # BuilderPage, BuilderSection, BuilderField 등
|
||||
│ └── constants.ts # 필드 타입, 섹션 타입 옵션 상수
|
||||
│
|
||||
└── utils/
|
||||
├── index.ts # 유틸 배럴 export
|
||||
└── transformers.ts # API ↔ Builder 타입 변환 함수
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 핵심 타입 정의
|
||||
|
||||
### 3.1 BuilderPage
|
||||
```typescript
|
||||
interface BuilderPage {
|
||||
id: string;
|
||||
name: string; // 페이지 이름 (예: "소모품 등록")
|
||||
itemType: ItemType; // 품목 유형 (FG, PT, SM, RM, CS)
|
||||
sections: BuilderSection[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 BuilderSection
|
||||
```typescript
|
||||
interface BuilderSection {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
sectionType: SectionType; // BASIC, BOM, CERTIFICATION 등
|
||||
columns: 1 | 2 | 3; // 레이아웃 열 수
|
||||
isCollapsible?: boolean;
|
||||
isDefaultOpen?: boolean;
|
||||
fields: BuilderField[];
|
||||
displayCondition?: DisplayCondition; // 조건부 표시
|
||||
order: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 BuilderField
|
||||
```typescript
|
||||
interface BuilderField {
|
||||
id: string;
|
||||
name: string; // 필드 라벨
|
||||
fieldKey: string; // API 키 (예: "item_name")
|
||||
inputType: FieldInputType; // textbox, number, dropdown 등
|
||||
required: boolean;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
options?: DropdownOption[]; // 드롭다운용
|
||||
colSpan?: 1 | 2 | 3;
|
||||
description?: string;
|
||||
displayCondition?: DisplayCondition;
|
||||
validationRules?: ValidationRule[];
|
||||
order: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 DisplayCondition (조건부 표시)
|
||||
```typescript
|
||||
interface DisplayCondition {
|
||||
enabled: boolean;
|
||||
logic: 'AND' | 'OR';
|
||||
conditions: FieldCondition[];
|
||||
}
|
||||
|
||||
interface FieldCondition {
|
||||
fieldKey: string;
|
||||
operator: 'equals' | 'not_equals' | 'contains' | 'not_contains';
|
||||
expectedValue: string;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. API 연동
|
||||
|
||||
### 4.1 사용하는 API
|
||||
품목기준관리와 **동일한 API** 사용:
|
||||
```typescript
|
||||
// src/lib/api/item-master.ts
|
||||
itemMasterApi.init() // 전체 페이지/섹션/필드 조회
|
||||
itemMasterApi.sections.create(pageId, data) // 섹션 생성
|
||||
itemMasterApi.sections.update(id, data) // 섹션 수정
|
||||
itemMasterApi.sections.delete(id) // 섹션 삭제
|
||||
itemMasterApi.fields.create(sectionId, data)// 필드 생성
|
||||
itemMasterApi.fields.update(id, data) // 필드 수정
|
||||
itemMasterApi.fields.delete(id) // 필드 삭제
|
||||
```
|
||||
|
||||
### 4.2 API 모드 토글
|
||||
- **API 모드 ON**: 백엔드 API에서 데이터 로드/저장
|
||||
- **API 모드 OFF**: localStorage에서 로드/저장 (오프라인 테스트용)
|
||||
- 설정은 localStorage에 저장되어 새로고침 후에도 유지
|
||||
|
||||
### 4.3 동기화 로직 (usePageManager.ts)
|
||||
```typescript
|
||||
const syncPageToAPI = async (page: BuilderPage) => {
|
||||
// 1. 원본 데이터와 현재 데이터 비교
|
||||
// 2. 삭제된 섹션/필드 → API DELETE 호출
|
||||
// 3. 새로 추가된 섹션/필드 → API CREATE 호출
|
||||
// 4. 수정된 섹션/필드 → API UPDATE 호출
|
||||
// 5. 완료 후 API에서 최신 데이터 다시 로드
|
||||
};
|
||||
```
|
||||
|
||||
### 4.4 타입 변환 (transformers.ts)
|
||||
```typescript
|
||||
// API → Builder 변환
|
||||
transformPagesToBuilder(apiData): BuilderPage[]
|
||||
|
||||
// Builder → API 변환
|
||||
transformSectionToAPI(section): APISectionCreateData
|
||||
transformFieldToAPI(field): APIFieldCreateData
|
||||
|
||||
// ID 구분 (API ID는 숫자, 로컬 ID는 문자열)
|
||||
isApiId(id: string): boolean // "123" → true, "section_abc123" → false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 주요 컴포넌트 설명
|
||||
|
||||
### 5.1 PageBuilderClient.tsx
|
||||
메인 컴포넌트로 전체 레이아웃 구성:
|
||||
- 상단: 툴바 (미리보기 모드, Undo/Redo, 저장 버튼들)
|
||||
- 좌측: PageSelector + ComponentPalette
|
||||
- 중앙: BuilderCanvas
|
||||
- 우측: PropertyPanel
|
||||
|
||||
주요 상태:
|
||||
```typescript
|
||||
const [isAPIMode, setIsAPIMode] = useState(true); // API 모드
|
||||
const [previewMode, setPreviewMode] = useState<'desktop' | 'tablet' | 'mobile'>('desktop');
|
||||
```
|
||||
|
||||
### 5.2 PageSelector.tsx
|
||||
페이지 목록 관리:
|
||||
- 페이지 선택/생성/삭제/복제/이름변경
|
||||
- 호버 시 액션 버튼 표시 (배경색 포함으로 긴 제목도 가리지 않음)
|
||||
|
||||
### 5.3 BuilderCanvas.tsx
|
||||
드래그 앤 드롭 캔버스:
|
||||
- 섹션 드롭 → 새 섹션 추가
|
||||
- 필드 드롭 → 해당 섹션에 필드 추가
|
||||
- 요소 클릭 → 선택 (PropertyPanel에서 편집)
|
||||
|
||||
### 5.4 PropertyPanel.tsx
|
||||
선택된 요소의 속성 편집:
|
||||
- 섹션: 제목, 설명, 타입, 열 수, 접기 설정, 조건부 표시
|
||||
- 필드: 이름, 키, 타입, 필수여부, 옵션, 조건부 표시
|
||||
|
||||
### 5.5 ConditionEditor.tsx
|
||||
조건부 표시 설정 UI:
|
||||
- 다른 필드 값에 따라 현재 섹션/필드 표시/숨김
|
||||
- AND/OR 논리 연산 지원
|
||||
- 여러 조건 추가 가능
|
||||
|
||||
---
|
||||
|
||||
## 6. 사용 방법
|
||||
|
||||
### 6.1 기본 워크플로우
|
||||
1. `/dev/page-builder` 접속
|
||||
2. **API 모드 ON** 확인 (우측 상단 토글)
|
||||
3. 좌측에서 페이지 선택 (소모품, 원자재 등)
|
||||
4. 컴포넌트 팔레트에서 섹션/필드를 캔버스로 드래그
|
||||
5. 캔버스에서 요소 클릭 → 우측 패널에서 속성 편집
|
||||
6. **"API 저장"** 버튼 클릭 → 백엔드에 저장
|
||||
7. 품목기준관리 페이지에서 변경사항 확인
|
||||
|
||||
### 6.2 저장 버튼 종류
|
||||
| 버튼 | 기능 |
|
||||
|------|------|
|
||||
| API 저장 (초록색) | 현재 페이지를 백엔드 API에 저장 |
|
||||
| 현재 페이지 저장 (JSON) | 현재 페이지를 JSON 파일로 다운로드 |
|
||||
| 전체 저장 | 모든 페이지를 JSON 파일로 다운로드 |
|
||||
| 가져오기 | JSON 파일에서 페이지 데이터 로드 |
|
||||
|
||||
### 6.3 Undo/Redo
|
||||
- **실행 취소**: 마지막 작업 취소
|
||||
- **다시 실행**: 취소한 작업 복원
|
||||
- 히스토리는 세션 동안만 유지
|
||||
|
||||
---
|
||||
|
||||
## 7. 테스트 완료 항목
|
||||
|
||||
### 7.1 API 연동 테스트 (2026-01-22)
|
||||
| 테스트 | 결과 |
|
||||
|--------|------|
|
||||
| 페이지 빌더에서 섹션 제목 수정 | ✅ "기본정보" → "기본정보 (빌더테스트)" |
|
||||
| API 저장 버튼 클릭 | ✅ `PUT /api/proxy/item-master/sections/92` (200 OK) |
|
||||
| 품목기준관리 페이지 반영 확인 | ✅ 변경된 제목 표시 확인 |
|
||||
|
||||
### 7.2 UI 수정 (2026-01-22)
|
||||
- 페이지 목록 호버 버튼에 배경색 추가 (긴 제목 가림 방지)
|
||||
|
||||
---
|
||||
|
||||
## 8. 알려진 이슈 / TODO
|
||||
|
||||
### 8.1 현재 이슈
|
||||
- [ ] 새 섹션/필드 생성 후 order 값 자동 계산 필요
|
||||
- [ ] BOM 섹션 타입 특수 처리 (자재명세표)
|
||||
- [ ] 드래그 앤 드롭 순서 변경 시 API order 업데이트
|
||||
|
||||
### 8.2 향후 개선 사항
|
||||
- [ ] 실시간 미리보기에서 실제 폼 렌더링 (DynamicItemForm 연동)
|
||||
- [ ] 필드 유효성 검사 규칙 편집 UI
|
||||
- [ ] 섹션/필드 복사-붙여넣기
|
||||
- [ ] 다중 선택 및 일괄 편집
|
||||
- [ ] 변경사항 diff 뷰어
|
||||
|
||||
### 8.3 Git 관련
|
||||
- 현재 `.gitignore`에 등록되어 있음: `src/app/**/dev/page-builder/`
|
||||
- 정식 배포 시 gitignore에서 제거 필요
|
||||
|
||||
---
|
||||
|
||||
## 9. 관련 파일
|
||||
|
||||
### 9.1 품목기준관리 (연동 대상)
|
||||
```
|
||||
src/app/[locale]/(protected)/master-data/item-master-data-management/page.tsx
|
||||
src/components/items/ItemMasterDataManagement/
|
||||
src/lib/api/item-master.ts
|
||||
```
|
||||
|
||||
### 9.2 동적 품목 폼 (렌더링 대상)
|
||||
```
|
||||
src/components/items/DynamicItemForm/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 참고 문서
|
||||
|
||||
- 초기 기획: `src/app/[locale]/(protected)/dev/page-builder/PLAN.md`
|
||||
- API 문서: `claudedocs/api/item-master-api.md` (있다면)
|
||||
|
||||
---
|
||||
|
||||
*마지막 업데이트: 2026-01-22*
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user