diff --git a/backend-node/data/mail-sent/12b583c9-a6b2-4c7f-8340-fd0e700aa32e.json b/backend-node/data/mail-sent/12b583c9-a6b2-4c7f-8340-fd0e700aa32e.json deleted file mode 100644 index 9e7a209c..00000000 --- a/backend-node/data/mail-sent/12b583c9-a6b2-4c7f-8340-fd0e700aa32e.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "12b583c9-a6b2-4c7f-8340-fd0e700aa32e", - "sentAt": "2025-10-22T05:17:38.303Z", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [ - "zian9227@naver.com" - ], - "subject": "Fwd: ㅏㅣ", - "htmlContent": "\r\n
\r\n

ㄴㅇㄹㄴㅇㄹㄴㅇㄹㅇ리'ㅐㅔ'ㅑ678463ㅎㄱ휼췇흍츄

\r\n
\r\n

\r\n
\r\n

---------- 전달된 메시지 ----------

\r\n

보낸 사람: \"이희진\"

\r\n

날짜: 2025. 10. 22. 오후 1:32:34

\r\n

제목: ㅏㅣ

\r\n
\r\n undefined\r\n
\r\n ", - "status": "success", - "messageId": "<74dbd467-6185-024d-dd60-bf4459ff9ea4@wace.me>", - "accepted": [ - "zian9227@naver.com" - ], - "rejected": [], - "deletedAt": "2025-10-22T06:36:10.876Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/1bb5ebfe-3f6c-4884-a043-161ae3f74f75.json b/backend-node/data/mail-sent/1bb5ebfe-3f6c-4884-a043-161ae3f74f75.json deleted file mode 100644 index 2f624e9c..00000000 --- a/backend-node/data/mail-sent/1bb5ebfe-3f6c-4884-a043-161ae3f74f75.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "1bb5ebfe-3f6c-4884-a043-161ae3f74f75", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [], - "cc": [], - "bcc": [], - "subject": "Fwd: ㄴㅇㄹㅇㄴㄴㄹ 테스트트트", - "htmlContent": "\n\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n전달된 메일:\n\n보낸사람: \"이희진\" \n날짜: 2025. 10. 22. 오후 4:24:54\n제목: ㄴㅇㄹㅇㄴㄴㄹ\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nㄹㅇㄴㄹㅇㄴㄹㅇㄴ\n", - "sentAt": "2025-10-22T07:49:50.811Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T07:49:50.811Z", - "deletedAt": "2025-10-22T07:50:14.211Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/375f2326-ca86-468a-bfc3-2d4c3825577b.json b/backend-node/data/mail-sent/375f2326-ca86-468a-bfc3-2d4c3825577b.json deleted file mode 100644 index c142808d..00000000 --- a/backend-node/data/mail-sent/375f2326-ca86-468a-bfc3-2d4c3825577b.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "375f2326-ca86-468a-bfc3-2d4c3825577b", - "sentAt": "2025-10-22T04:57:39.706Z", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [ - "\"이희진\" " - ], - "subject": "Re: ㅏㅣ", - "htmlContent": "\r\n
\r\n

ㅁㄴㅇㄹㅁㅇㄴㄹㅁㄴㅇㄹㅁㄴㅇㄹㅁㄴㅇㄹㅁㄴㅇㄹㄴㅁㅇㄹ

\r\n
\r\n

\r\n
\r\n

보낸 사람: \"이희진\"

\r\n

날짜: 2025. 10. 22. 오후 1:32:34

\r\n

제목: ㅏㅣ

\r\n
\r\n undefined\r\n
\r\n ", - "status": "success", - "messageId": "", - "accepted": [ - "zian9227@naver.com" - ], - "rejected": [], - "deletedAt": "2025-10-22T07:11:04.666Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/386e334a-df76-440c-ae8a-9bf06982fdc8.json b/backend-node/data/mail-sent/386e334a-df76-440c-ae8a-9bf06982fdc8.json deleted file mode 100644 index 31da5552..00000000 --- a/backend-node/data/mail-sent/386e334a-df76-440c-ae8a-9bf06982fdc8.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "386e334a-df76-440c-ae8a-9bf06982fdc8", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [], - "cc": [], - "bcc": [], - "subject": "Fwd: ㄴ", - "htmlContent": "\n

\n
\n

---------- 전달된 메일 ----------

\n

보낸사람: \"이희진\" <zian9227@naver.com>

\n

날짜: 2025. 10. 22. 오후 12:58:15

\n

제목:

\n
\n

ㄴㅇㄹㄴㅇㄹㄴㅇㄹ\n

\n
\n ", - "sentAt": "2025-10-22T07:04:27.192Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T07:04:57.280Z", - "deletedAt": "2025-10-22T07:50:17.136Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/3d411dc4-69a6-4236-b878-9693dff881be.json b/backend-node/data/mail-sent/3d411dc4-69a6-4236-b878-9693dff881be.json deleted file mode 100644 index aa107de7..00000000 --- a/backend-node/data/mail-sent/3d411dc4-69a6-4236-b878-9693dff881be.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "3d411dc4-69a6-4236-b878-9693dff881be", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [ - "zian9227@naver.com" - ], - "cc": [], - "bcc": [], - "subject": "Re: ㄴ", - "htmlContent": "\n

\n
\n

원본 메일:

\n

보낸사람: \"이희진\"

\n

날짜: 2025. 10. 22. 오후 12:58:15

\n

제목:

\n
\n

undefined

\n
\n ", - "sentAt": "2025-10-22T06:56:51.060Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T06:56:51.060Z", - "deletedAt": "2025-10-22T07:50:22.989Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/3e30a264-8431-44c7-96ef-eed551e66a11.json b/backend-node/data/mail-sent/3e30a264-8431-44c7-96ef-eed551e66a11.json deleted file mode 100644 index d824d67b..00000000 --- a/backend-node/data/mail-sent/3e30a264-8431-44c7-96ef-eed551e66a11.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "3e30a264-8431-44c7-96ef-eed551e66a11", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [], - "cc": [], - "bcc": [], - "subject": "Fwd: ㄴ", - "htmlContent": "\n

\n
\n

---------- 전달된 메일 ----------

\n

보낸사람: \"이희진\"

\n

날짜: 2025. 10. 22. 오후 12:58:15

\n

제목:

\n
\n

\n
\n ", - "sentAt": "2025-10-22T06:57:53.335Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T07:00:23.394Z", - "deletedAt": "2025-10-22T07:50:20.510Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/4a32bab5-364e-4037-bb00-31d2905824db.json b/backend-node/data/mail-sent/4a32bab5-364e-4037-bb00-31d2905824db.json deleted file mode 100644 index 92de4a0c..00000000 --- a/backend-node/data/mail-sent/4a32bab5-364e-4037-bb00-31d2905824db.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "4a32bab5-364e-4037-bb00-31d2905824db", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [], - "cc": [], - "bcc": [], - "subject": "테스트 마지가", - "htmlContent": "ㅁㄴㅇㄹ", - "sentAt": "2025-10-22T07:49:29.948Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T07:49:29.948Z", - "deletedAt": "2025-10-22T07:50:12.374Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/5bfb2acd-023a-4865-a738-2900179db5fb.json b/backend-node/data/mail-sent/5bfb2acd-023a-4865-a738-2900179db5fb.json deleted file mode 100644 index 5f5a5cfc..00000000 --- a/backend-node/data/mail-sent/5bfb2acd-023a-4865-a738-2900179db5fb.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "5bfb2acd-023a-4865-a738-2900179db5fb", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [], - "cc": [], - "bcc": [], - "subject": "Fwd: ㄴ", - "htmlContent": "\n

\n
\n

---------- 전달된 메일 ----------

\n

보낸사람: \"이희진\"

\n

날짜: 2025. 10. 22. 오후 12:58:15

\n

제목:

\n
\n

ㄴㅇㄹㄴㅇㄹㄴㅇㄹ\n

\n
\n ", - "sentAt": "2025-10-22T07:03:09.080Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T07:03:39.150Z", - "deletedAt": "2025-10-22T07:50:19.035Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/683c1323-1895-403a-bb9a-4e111a8909f6.json b/backend-node/data/mail-sent/683c1323-1895-403a-bb9a-4e111a8909f6.json deleted file mode 100644 index b3c3259f..00000000 --- a/backend-node/data/mail-sent/683c1323-1895-403a-bb9a-4e111a8909f6.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "683c1323-1895-403a-bb9a-4e111a8909f6", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [ - "zian9227@naver.com" - ], - "cc": [], - "bcc": [], - "subject": "Re: ㄴ", - "htmlContent": "\n

\n
\n

원본 메일:

\n

보낸사람: \"이희진\"

\n

날짜: 2025. 10. 22. 오후 12:58:15

\n

제목:

\n
\n

undefined

\n
\n ", - "sentAt": "2025-10-22T06:54:55.097Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T06:54:55.097Z", - "deletedAt": "2025-10-22T07:50:24.672Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/7bed27d5-dae4-4ba8-85d0-c474c4fb907a.json b/backend-node/data/mail-sent/7bed27d5-dae4-4ba8-85d0-c474c4fb907a.json deleted file mode 100644 index d9edbdeb..00000000 --- a/backend-node/data/mail-sent/7bed27d5-dae4-4ba8-85d0-c474c4fb907a.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "7bed27d5-dae4-4ba8-85d0-c474c4fb907a", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [], - "cc": [], - "bcc": [], - "subject": "Fwd: ㅏㅣ", - "htmlContent": "\n

\n
\n

---------- 전달된 메일 ----------

\n

보낸사람: \"이희진\"

\n

날짜: 2025. 10. 22. 오후 1:32:34

\n

제목: ㅏㅣ

\n
\n undefined\n
\n ", - "sentAt": "2025-10-22T06:41:52.984Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T06:46:23.051Z", - "deletedAt": "2025-10-22T07:50:29.124Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/8990ea86-3112-4e7c-b3e0-8b494181c4e0.json b/backend-node/data/mail-sent/8990ea86-3112-4e7c-b3e0-8b494181c4e0.json deleted file mode 100644 index f0ed2dcf..00000000 --- a/backend-node/data/mail-sent/8990ea86-3112-4e7c-b3e0-8b494181c4e0.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "id": "8990ea86-3112-4e7c-b3e0-8b494181c4e0", - "accountName": "", - "accountEmail": "", - "to": [], - "subject": "", - "htmlContent": "", - "sentAt": "2025-10-22T06:17:31.379Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T06:17:31.379Z", - "deletedAt": "2025-10-22T07:50:30.736Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/99703f2c-740c-492e-a866-a04289a9b699.json b/backend-node/data/mail-sent/99703f2c-740c-492e-a866-a04289a9b699.json deleted file mode 100644 index 1c6dc41f..00000000 --- a/backend-node/data/mail-sent/99703f2c-740c-492e-a866-a04289a9b699.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "id": "99703f2c-740c-492e-a866-a04289a9b699", - "accountName": "", - "accountEmail": "", - "to": [], - "subject": "", - "htmlContent": "", - "sentAt": "2025-10-22T06:20:08.450Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T06:20:08.450Z", - "deletedAt": "2025-10-22T06:36:07.797Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/9ab1e5ee-4f5e-4b79-9769-5e2a1e1ffc8e.json b/backend-node/data/mail-sent/9ab1e5ee-4f5e-4b79-9769-5e2a1e1ffc8e.json deleted file mode 100644 index 31bde67a..00000000 --- a/backend-node/data/mail-sent/9ab1e5ee-4f5e-4b79-9769-5e2a1e1ffc8e.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "9ab1e5ee-4f5e-4b79-9769-5e2a1e1ffc8e", - "sentAt": "2025-10-22T04:31:17.175Z", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [ - "\"이희진\" " - ], - "subject": "Re: ㅅㄷㄴㅅ", - "htmlContent": "\r\n
\r\n

배불르고 졸린데 커피먹으니깐 졸린건 괜찮아졋고 배불러서 물배찼당아아아아

\r\n
\r\n

\r\n
\r\n

보낸 사람: \"이희진\"

\r\n

날짜: 2025. 10. 22. 오후 1:03:03

\r\n

제목: ㅅㄷㄴㅅ

\r\n
\r\n undefined\r\n
\r\n ", - "status": "success", - "messageId": "<0f215ba8-a1e4-8c5a-f43f-962f0717c161@wace.me>", - "accepted": [ - "zian9227@naver.com" - ], - "rejected": [], - "deletedAt": "2025-10-22T07:11:10.245Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/9d0b9fcf-cabf-4053-b6b6-6e110add22de.json b/backend-node/data/mail-sent/9d0b9fcf-cabf-4053-b6b6-6e110add22de.json deleted file mode 100644 index 2ace7d67..00000000 --- a/backend-node/data/mail-sent/9d0b9fcf-cabf-4053-b6b6-6e110add22de.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "9d0b9fcf-cabf-4053-b6b6-6e110add22de", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [ - "zian9227@naver.com" - ], - "cc": [], - "bcc": [], - "subject": "Re: ㅏㅣ", - "htmlContent": "\n

\n
\n

원본 메일:

\n

보낸사람: \"이희진\"

\n

날짜: 2025. 10. 22. 오후 1:32:34

\n

제목: ㅏㅣ

\n
\n

undefined

\n
\n ", - "sentAt": "2025-10-22T06:50:04.224Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T06:50:04.224Z", - "deletedAt": "2025-10-22T07:50:26.224Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/b293e530-2b2d-4b8a-8081-d103fab5a13f.json b/backend-node/data/mail-sent/b293e530-2b2d-4b8a-8081-d103fab5a13f.json deleted file mode 100644 index 77d9053f..00000000 --- a/backend-node/data/mail-sent/b293e530-2b2d-4b8a-8081-d103fab5a13f.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "b293e530-2b2d-4b8a-8081-d103fab5a13f", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [ - "zian9227@naver.com" - ], - "cc": [], - "bcc": [], - "subject": "Re: 수신메일확인용", - "htmlContent": "\n

\n
\n

원본 메일:

\n

보낸사람: \"이희진\"

\n

날짜: 2025. 10. 13. 오전 10:40:30

\n

제목: 수신메일확인용

\n
\n undefined\n
\n ", - "sentAt": "2025-10-22T06:47:53.815Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T06:48:53.876Z", - "deletedAt": "2025-10-22T07:50:27.706Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/cf892a77-1998-4165-bb9d-b390451465b2.json b/backend-node/data/mail-sent/cf892a77-1998-4165-bb9d-b390451465b2.json deleted file mode 100644 index 426f81fb..00000000 --- a/backend-node/data/mail-sent/cf892a77-1998-4165-bb9d-b390451465b2.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "cf892a77-1998-4165-bb9d-b390451465b2", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [], - "cc": [], - "bcc": [], - "subject": "Fwd: ㄴ", - "htmlContent": "\n\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n전달된 메일:\n\n보낸사람: \"이희진\" \n날짜: 2025. 10. 22. 오후 12:58:15\n제목: ㄴ\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nㄴㅇㄹㄴㅇㄹㄴㅇㄹ\n", - "sentAt": "2025-10-22T07:06:11.620Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T07:07:11.749Z", - "deletedAt": "2025-10-22T07:50:15.739Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/e3501abc-cd31-4b20-bb02-3c7ddbe54eb8.json b/backend-node/data/mail-sent/e3501abc-cd31-4b20-bb02-3c7ddbe54eb8.json deleted file mode 100644 index cf31f7dc..00000000 --- a/backend-node/data/mail-sent/e3501abc-cd31-4b20-bb02-3c7ddbe54eb8.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "id": "e3501abc-cd31-4b20-bb02-3c7ddbe54eb8", - "accountName": "", - "accountEmail": "", - "to": [], - "subject": "", - "htmlContent": "", - "sentAt": "2025-10-22T06:15:02.128Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T06:15:02.128Z", - "deletedAt": "2025-10-22T07:08:43.543Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/eb92ed00-cc4f-4cc8-94c9-9bef312d16db.json b/backend-node/data/mail-sent/eb92ed00-cc4f-4cc8-94c9-9bef312d16db.json deleted file mode 100644 index 0c19dc0c..00000000 --- a/backend-node/data/mail-sent/eb92ed00-cc4f-4cc8-94c9-9bef312d16db.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "eb92ed00-cc4f-4cc8-94c9-9bef312d16db", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [], - "cc": [], - "bcc": [], - "subject": "메일 임시저장 테스트 4", - "htmlContent": "asd", - "sentAt": "2025-10-22T06:21:40.019Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T06:21:40.019Z", - "deletedAt": "2025-10-22T06:36:05.306Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/fd2a8b41-2e6e-4e5e-b8e8-63d31efc5082.json b/backend-node/data/mail-sent/fd2a8b41-2e6e-4e5e-b8e8-63d31efc5082.json deleted file mode 100644 index 073c20f0..00000000 --- a/backend-node/data/mail-sent/fd2a8b41-2e6e-4e5e-b8e8-63d31efc5082.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "id": "fd2a8b41-2e6e-4e5e-b8e8-63d31efc5082", - "sentAt": "2025-10-22T04:29:14.738Z", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [ - "\"이희진\" " - ], - "subject": "Re: ㅅㄷㄴㅅ", - "htmlContent": "\r\n
\r\n

ㅁㄴㅇㄹㅁㄴㅇㄹㅁㄴㅇㄹㅁㄴㅇㄹㄴㅇㄹㄴㅇㄹ

\r\n
\r\n

\r\n
\r\n

보낸 사람: \"이희진\"

\r\n

날짜: 2025. 10. 22. 오후 1:03:03

\r\n

제목: ㅅㄷㄴㅅ

\r\n
\r\n undefined\r\n
\r\n ", - "attachments": [ - { - "filename": "test용 이미지2.png", - "originalName": "test용 이미지2.png", - "size": 0, - "path": "/app/uploads/mail-attachments/1761107350246-298369766.png", - "mimetype": "image/png" - } - ], - "status": "success", - "messageId": "", - "accepted": [ - "zian9227@naver.com" - ], - "rejected": [], - "deletedAt": "2025-10-22T07:11:12.907Z" -} \ No newline at end of file diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index e9485620..c40037bb 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -99,10 +99,18 @@ export class DynamicFormService { } try { - // YYYY-MM-DD 형식인 경우 시간 추가해서 Date 객체 생성 + // YYYY-MM-DD 형식인 경우 if (/^\d{4}-\d{2}-\d{2}$/.test(value)) { - console.log(`📅 날짜 타입 변환: ${value} -> Date 객체`); - return new Date(value + "T00:00:00"); + // DATE 타입이면 문자열 그대로 유지 + if (lowerDataType === "date") { + console.log(`📅 날짜 문자열 유지: ${value} -> "${value}" (DATE 타입)`); + return value; // 문자열 그대로 반환 + } + // TIMESTAMP 타입이면 Date 객체로 변환 + else { + console.log(`📅 날짜시간 변환: ${value} -> Date 객체 (TIMESTAMP 타입)`); + return new Date(value + "T00:00:00"); + } } // 다른 날짜 형식도 Date 객체로 변환 else { @@ -300,13 +308,13 @@ export class DynamicFormService { ) { // YYYY-MM-DD HH:mm:ss 형태의 문자열을 Date 객체로 변환 if (value.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)) { - console.log(`📅 날짜 변환: ${key} = "${value}" -> Date 객체`); + console.log(`📅 날짜시간 변환: ${key} = "${value}" -> Date 객체`); dataToInsert[key] = new Date(value); } - // YYYY-MM-DD 형태의 문자열을 Date 객체로 변환 + // YYYY-MM-DD 형태의 문자열은 그대로 유지 (DATE 타입으로 저장) else if (value.match(/^\d{4}-\d{2}-\d{2}$/)) { - console.log(`📅 날짜 변환: ${key} = "${value}" -> Date 객체`); - dataToInsert[key] = new Date(value + "T00:00:00"); + console.log(`📅 날짜 유지: ${key} = "${value}" -> 문자열 그대로 (DATE 타입)`); + // dataToInsert[key] = value; // 문자열 그대로 유지 (이미 올바른 형식) } } }); @@ -849,10 +857,22 @@ export class DynamicFormService { const values: any[] = Object.values(changedFields); values.push(id); // WHERE 조건용 ID 추가 + // 🔑 Primary Key 타입에 맞게 캐스팅 + const pkDataType = columnTypes[primaryKeyColumn]; + let pkCast = ''; + if (pkDataType === 'integer' || pkDataType === 'bigint' || pkDataType === 'smallint') { + pkCast = '::integer'; + } else if (pkDataType === 'numeric' || pkDataType === 'decimal') { + pkCast = '::numeric'; + } else if (pkDataType === 'uuid') { + pkCast = '::uuid'; + } + // text, varchar 등은 캐스팅 불필요 + const updateQuery = ` UPDATE ${tableName} SET ${setClause} - WHERE ${primaryKeyColumn} = $${values.length}::text + WHERE ${primaryKeyColumn} = $${values.length}${pkCast} RETURNING * `; diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index ce99a685..5685d23a 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -356,17 +356,6 @@ function ScreenViewPage() { return isButton; }); - console.log( - "🔍 메뉴에서 발견된 전체 버튼:", - allButtons.map((b) => ({ - id: b.id, - label: b.label, - positionX: b.position.x, - positionY: b.position.y, - width: b.size?.width, - height: b.size?.height, - })), - ); topLevelComponents.forEach((component) => { const isButton = @@ -406,33 +395,13 @@ function ScreenViewPage() { (c) => (c as any).componentId === "table-search-widget", ); - // 디버그: 모든 컴포넌트 타입 확인 - console.log( - "🔍 전체 컴포넌트 타입:", - regularComponents.map((c) => ({ - id: c.id, - type: c.type, - componentType: (c as any).componentType, - componentId: (c as any).componentId, - })), - ); - - // 🆕 조건부 컨테이너들을 찾기 + // 조건부 컨테이너들을 찾기 const conditionalContainers = regularComponents.filter( (c) => (c as any).componentId === "conditional-container" || (c as any).componentType === "conditional-container", ); - console.log( - "🔍 조건부 컨테이너 발견:", - conditionalContainers.map((c) => ({ - id: c.id, - y: c.position.y, - size: c.size, - })), - ); - // TableSearchWidget 및 조건부 컨테이너 높이 차이를 계산하여 Y 위치 조정 const adjustedComponents = regularComponents.map((component) => { const isTableSearchWidget = (component as any).componentId === "table-search-widget"; @@ -520,12 +489,6 @@ function ScreenViewPage() { columnOrder={tableColumnOrder} tableDisplayData={tableDisplayData} onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder, tableDisplayData) => { - console.log("🔍 화면에서 선택된 행 데이터:", selectedData); - console.log("📊 정렬 정보:", { sortBy, sortOrder, columnOrder }); - console.log("📊 화면 표시 데이터:", { - count: tableDisplayData?.length, - firstRow: tableDisplayData?.[0], - }); setSelectedRowsData(selectedData); setTableSortBy(sortBy); setTableSortOrder(sortOrder || "asc"); @@ -604,12 +567,6 @@ function ScreenViewPage() { columnOrder, tableDisplayData, ) => { - console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData); - console.log("📊 정렬 정보 (자식):", { sortBy, sortOrder, columnOrder }); - console.log("📊 화면 표시 데이터 (자식):", { - count: tableDisplayData?.length, - firstRow: tableDisplayData?.[0], - }); setSelectedRowsData(selectedData); setTableSortBy(sortBy); setTableSortOrder(sortOrder || "asc"); @@ -618,7 +575,6 @@ function ScreenViewPage() { }} refreshKey={tableRefreshKey} onRefresh={() => { - console.log("🔄 테이블 새로고침 요청됨 (자식)"); setTableRefreshKey((prev) => prev + 1); setSelectedRowsData([]); // 선택 해제 }} diff --git a/frontend/components/order/OrderRegistrationModal.tsx b/frontend/components/order/OrderRegistrationModal.tsx index bd780038..615f0426 100644 --- a/frontend/components/order/OrderRegistrationModal.tsx +++ b/frontend/components/order/OrderRegistrationModal.tsx @@ -64,6 +64,9 @@ export function OrderRegistrationModal({ // 선택된 품목 목록 const [selectedItems, setSelectedItems] = useState([]); + // 납기일 일괄 적용 플래그 (딱 한 번만 실행) + const [isDeliveryDateApplied, setIsDeliveryDateApplied] = useState(false); + // 저장 중 const [isSaving, setIsSaving] = useState(false); @@ -158,6 +161,45 @@ export function OrderRegistrationModal({ hsCode: "", }); setSelectedItems([]); + setIsDeliveryDateApplied(false); // 플래그 초기화 + }; + + // 품목 목록 변경 핸들러 (납기일 일괄 적용 로직 포함) + const handleItemsChange = (newItems: any[]) => { + // 1️⃣ 플래그가 이미 true면 그냥 업데이트만 (일괄 적용 완료 상태) + if (isDeliveryDateApplied) { + setSelectedItems(newItems); + return; + } + + // 2️⃣ 품목이 없으면 그냥 업데이트 + if (newItems.length === 0) { + setSelectedItems(newItems); + return; + } + + // 3️⃣ 현재 상태: 납기일이 있는 행과 없는 행 개수 체크 + const itemsWithDate = newItems.filter((item) => item.delivery_date); + const itemsWithoutDate = newItems.filter((item) => !item.delivery_date); + + // 4️⃣ 조건: 정확히 1개만 날짜가 있고, 나머지는 모두 비어있을 때 일괄 적용 + if (itemsWithDate.length === 1 && itemsWithoutDate.length > 0) { + // 5️⃣ 전체 일괄 적용 + const selectedDate = itemsWithDate[0].delivery_date; + const updatedItems = newItems.map((item) => ({ + ...item, + delivery_date: selectedDate, // 모든 행에 동일한 납기일 적용 + })); + + setSelectedItems(updatedItems); + setIsDeliveryDateApplied(true); // 플래그 활성화 (다음부터는 일괄 적용 안 함) + + console.log("✅ 납기일 일괄 적용 완료:", selectedDate); + console.log(` - 대상: ${itemsWithoutDate.length}개 행에 ${selectedDate} 적용`); + } else { + // 그냥 업데이트 + setSelectedItems(newItems); + } }; // 전체 금액 계산 @@ -338,7 +380,7 @@ export function OrderRegistrationModal({ diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index f9b803b2..9945a19c 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -316,6 +316,33 @@ export const EditModal: React.FC = ({ className }) => { screenId: modalState.screenId, }); + // 🆕 날짜 필드 정규화 함수 (YYYY-MM-DD 형식으로 변환) + const normalizeDateField = (value: any): string | null => { + if (!value) return null; + + // ISO 8601 형식 (2025-11-26T00:00:00.000Z) 또는 Date 객체 + if (value instanceof Date || typeof value === "string") { + try { + const date = new Date(value); + if (isNaN(date.getTime())) return null; + + // YYYY-MM-DD 형식으로 변환 + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + } catch (error) { + console.warn("날짜 변환 실패:", value, error); + return null; + } + } + + return null; + }; + + // 날짜 필드 목록 + const dateFields = ["item_due_date", "delivery_date", "due_date", "order_date"]; + let insertedCount = 0; let updatedCount = 0; let deletedCount = 0; @@ -333,6 +360,17 @@ export const EditModal: React.FC = ({ className }) => { delete insertData.id; // id는 자동 생성되므로 제거 + // 🆕 날짜 필드 정규화 (YYYY-MM-DD 형식으로 변환) + dateFields.forEach((fieldName) => { + if (insertData[fieldName]) { + const normalizedDate = normalizeDateField(insertData[fieldName]); + if (normalizedDate) { + insertData[fieldName] = normalizedDate; + console.log(`📅 [날짜 정규화] ${fieldName}: ${currentData[fieldName]} → ${normalizedDate}`); + } + } + }); + // 🆕 groupByColumns의 값을 강제로 포함 (order_no 등) if (modalState.groupByColumns && modalState.groupByColumns.length > 0) { modalState.groupByColumns.forEach((colName) => { @@ -348,23 +386,32 @@ export const EditModal: React.FC = ({ className }) => { // 🆕 공통 필드 추가 (거래처, 담당자, 납품처, 메모 등) // formData에서 품목별 필드가 아닌 공통 필드를 복사 const commonFields = [ - 'partner_id', // 거래처 - 'manager_id', // 담당자 - 'delivery_partner_id', // 납품처 - 'delivery_address', // 납품장소 - 'memo', // 메모 - 'order_date', // 주문일 - 'due_date', // 납기일 - 'shipping_method', // 배송방법 - 'status', // 상태 - 'sales_type', // 영업유형 + "partner_id", // 거래처 + "manager_id", // 담당자 + "delivery_partner_id", // 납품처 + "delivery_address", // 납품장소 + "memo", // 메모 + "order_date", // 주문일 + "due_date", // 납기일 + "shipping_method", // 배송방법 + "status", // 상태 + "sales_type", // 영업유형 ]; commonFields.forEach((fieldName) => { // formData에 값이 있으면 추가 if (formData[fieldName] !== undefined && formData[fieldName] !== null) { - insertData[fieldName] = formData[fieldName]; - console.log(`🔗 [공통 필드] ${fieldName} 값 추가:`, formData[fieldName]); + // 날짜 필드인 경우 정규화 + if (dateFields.includes(fieldName)) { + const normalizedDate = normalizeDateField(formData[fieldName]); + if (normalizedDate) { + insertData[fieldName] = normalizedDate; + console.log(`🔗 [공통 필드 - 날짜] ${fieldName} 값 추가:`, normalizedDate); + } + } else { + insertData[fieldName] = formData[fieldName]; + console.log(`🔗 [공통 필드] ${fieldName} 값 추가:`, formData[fieldName]); + } } }); @@ -404,8 +451,15 @@ export const EditModal: React.FC = ({ className }) => { } // 🆕 값 정규화 함수 (타입 통일) - const normalizeValue = (val: any): any => { + const normalizeValue = (val: any, fieldName?: string): any => { if (val === null || val === undefined || val === "") return null; + + // 날짜 필드인 경우 YYYY-MM-DD 형식으로 정규화 + if (fieldName && dateFields.includes(fieldName)) { + const normalizedDate = normalizeDateField(val); + return normalizedDate; + } + if (typeof val === "string" && !isNaN(Number(val))) { // 숫자로 변환 가능한 문자열은 숫자로 return Number(val); @@ -422,13 +476,14 @@ export const EditModal: React.FC = ({ className }) => { } // 🆕 타입 정규화 후 비교 - const currentValue = normalizeValue(currentData[key]); - const originalValue = normalizeValue(originalItemData[key]); + const currentValue = normalizeValue(currentData[key], key); + const originalValue = normalizeValue(originalItemData[key], key); // 값이 변경된 경우만 포함 if (currentValue !== originalValue) { console.log(`🔍 [품목 수정 감지] ${key}: ${originalValue} → ${currentValue}`); - changedData[key] = currentData[key]; // 원본 값 사용 (문자열 그대로) + // 날짜 필드는 정규화된 값 사용, 나머지는 원본 값 사용 + changedData[key] = dateFields.includes(key) ? currentValue : currentData[key]; } }); @@ -631,13 +686,6 @@ export const EditModal: React.FC = ({ className }) => { maxHeight: "100%", }} > - {/* 🆕 그룹 데이터가 있으면 안내 메시지 표시 */} - {groupData.length > 1 && ( -
- {groupData.length}개의 관련 품목을 함께 수정합니다 -
- )} - {screenData.components.map((component) => { // 컴포넌트 위치를 offset만큼 조정 const offsetX = screenDimensions?.offsetX || 0; diff --git a/frontend/components/screen/ScreenList.tsx b/frontend/components/screen/ScreenList.tsx index d2d3e367..8e5ba1d2 100644 --- a/frontend/components/screen/ScreenList.tsx +++ b/frontend/components/screen/ScreenList.tsx @@ -47,6 +47,9 @@ import dynamic from "next/dynamic"; import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; import { DynamicWebTypeRenderer } from "@/lib/registry"; import { isFileComponent, getComponentWebType } from "@/lib/utils/componentTypeUtils"; +import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; +import { RealtimePreview } from "./RealtimePreviewDynamic"; +import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext"; // InteractiveScreenViewer를 동적으로 import (SSR 비활성화) const InteractiveScreenViewer = dynamic( @@ -1315,24 +1318,40 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr 화면 미리보기 - {screenToPreview?.screenName} -
- {isLoadingPreview ? ( -
-
-
레이아웃 로딩 중...
-
화면 정보를 불러오고 있습니다.
+ + +
+ {isLoadingPreview ? ( +
+
+
레이아웃 로딩 중...
+
화면 정보를 불러오고 있습니다.
+
-
- ) : previewLayout && previewLayout.components ? ( + ) : previewLayout && previewLayout.components ? ( (() => { const screenWidth = previewLayout.screenResolution?.width || 1200; const screenHeight = previewLayout.screenResolution?.height || 800; // 모달 내부 가용 공간 계산 (헤더, 푸터, 패딩 제외) - const availableWidth = typeof window !== "undefined" ? window.innerWidth * 0.95 - 100 : 1800; // 95vw - 패딩 + const modalPadding = 100; // 헤더 + 푸터 + 패딩 + const availableWidth = typeof window !== "undefined" ? window.innerWidth * 0.95 - modalPadding : 1700; + const availableHeight = typeof window !== "undefined" ? window.innerHeight * 0.95 - modalPadding : 900; - // 가로폭 기준으로 스케일 계산 (가로폭에 맞춤) - const scale = availableWidth / screenWidth; + // 가로/세로 비율을 모두 고려하여 작은 쪽에 맞춤 (화면이 잘리지 않도록) + const scaleX = availableWidth / screenWidth; + const scaleY = availableHeight / screenHeight; + const scale = Math.min(scaleX, scaleY, 1); // 최대 1배율 (확대 방지) + + console.log("📐 미리보기 스케일 계산:", { + screenWidth, + screenHeight, + availableWidth, + availableHeight, + scaleX, + scaleY, + finalScale: scale, + }); return (
- {/* 라벨을 외부에 별도로 렌더링 */} - {shouldShowLabel && ( -
- {labelText} - {component.required && *} -
- )} + {}} + screenId={screenToPreview!.screenId} + tableName={screenToPreview?.tableName} + formData={previewFormData} + onFormDataChange={(fieldName, value) => { + setPreviewFormData((prev) => ({ + ...prev, + [fieldName]: value, + })); + }} + > + {/* 자식 컴포넌트들 */} + {(component.type === "group" || + component.type === "container" || + component.type === "area") && + previewLayout.components + .filter((child: any) => child.parentId === component.id) + .map((child: any) => { + // 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정 + const relativeChildComponent = { + ...child, + position: { + x: child.position.x - component.position.x, + y: child.position.y - component.position.y, + z: child.position.z || 1, + }, + }; - {/* 실제 컴포넌트 */} -
{ - const style = { - position: "absolute" as const, - left: `${component.position.x}px`, - top: `${component.position.y}px`, - width: component.style?.width || `${component.size.width}px`, - height: component.style?.height || `${component.size.height}px`, - zIndex: component.position.z || 1, - }; - - return style; - })()} - > - {/* 위젯 컴포넌트가 아닌 경우 DynamicComponentRenderer 사용 */} - {component.type !== "widget" ? ( - { - setPreviewFormData((prev) => ({ - ...prev, - [fieldName]: value, - })); - }} - screenId={screenToPreview!.screenId} - tableName={screenToPreview?.tableName} - /> - ) : ( - { - // 유틸리티 함수로 파일 컴포넌트 감지 - if (isFileComponent(component)) { - return "file"; - } - // 다른 컴포넌트는 유틸리티 함수로 webType 결정 - return getComponentWebType(component) || "text"; - })()} - config={component.webTypeConfig} - props={{ - component: component, - value: previewFormData[component.columnName || component.id] || "", - onChange: (value: any) => { - const fieldName = component.columnName || component.id; - setPreviewFormData((prev) => ({ - ...prev, - [fieldName]: value, - })); - }, - onFormDataChange: (fieldName, value) => { - setPreviewFormData((prev) => ({ - ...prev, - [fieldName]: value, - })); - }, - isInteractive: true, - formData: previewFormData, - readonly: component.readonly, - required: component.required, - placeholder: component.placeholder, - className: "w-full h-full", - }} - /> - )} -
-
+ return ( + {}} + screenId={screenToPreview!.screenId} + tableName={screenToPreview?.tableName} + formData={previewFormData} + onFormDataChange={(fieldName, value) => { + setPreviewFormData((prev) => ({ + ...prev, + [fieldName]: value, + })); + }} + /> + ); + })} + ); })}
@@ -1536,7 +1501,9 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
)} - + + + @@ -1702,30 +1978,40 @@ export const SelectedItemsDetailInputConfigPanel: React.FC {/* 2단계: 카테고리 선택 */} - {(config.autoCalculation.valueMapping as any)?._selectedMenus?.discountType && ( -
- - -
- )} + {(() => { + const hasSelectedMenu = !!(config.autoCalculation.valueMapping as any)?._selectedMenus?.discountType; + const columns = categoryColumns.discountType || []; + console.log("🎨 [렌더링] 2단계 카테고리 선택", { + hasSelectedMenu, + columns, + columnsCount: columns.length, + categoryColumnsState: categoryColumns + }); + return hasSelectedMenu ? ( +
+ + +
+ ) : null; + })()} {/* 3단계: 값 매핑 */} {(config.autoCalculation.valueMapping as any)?._selectedCategories?.discountType && ( @@ -1780,14 +2066,21 @@ export const SelectedItemsDetailInputConfigPanel: React.FC {/* 반올림 방식 매핑 */} - + setExpandedCategoryMappings(prev => ({ ...prev, roundingType: open }))} + > @@ -1890,14 +2183,21 @@ export const SelectedItemsDetailInputConfigPanel: React.FC {/* 반올림 단위 매핑 */} - + setExpandedCategoryMappings(prev => ({ ...prev, roundingUnit: open }))} + > @@ -2235,10 +2535,10 @@ export const SelectedItemsDetailInputConfigPanel: React.FC {mapping.targetField - ? targetTableColumns.find((c) => c.columnName === mapping.targetField)?.columnLabel || + ? loadedTargetTableColumns.find((c) => c.columnName === mapping.targetField)?.columnLabel || mapping.targetField : "저장 테이블 컬럼 선택"} @@ -2248,13 +2548,15 @@ export const SelectedItemsDetailInputConfigPanel: React.FC - {targetTableColumns.length === 0 ? ( - 저장 테이블을 먼저 선택하세요 + {!config.targetTable ? ( + 저장 대상 테이블을 먼저 선택하세요 + ) : loadedTargetTableColumns.length === 0 ? ( + 컬럼 로딩 중... ) : ( <> 컬럼을 찾을 수 없습니다. - {targetTableColumns.map((col) => { + {loadedTargetTableColumns.map((col) => { const searchValue = `${col.columnLabel || col.columnName} ${col.columnName} ${col.dataType || ""}`.toLowerCase(); return ( {col.columnLabel || col.columnName} {col.dataType && ( - {col.dataType} + + {col.dataType} + )} @@ -2289,17 +2593,27 @@ export const SelectedItemsDetailInputConfigPanel: React.FC +

+ 현재 화면의 저장 대상 테이블 ({config.targetTable || "미선택"})의 컬럼 +

{/* 기본값 (선택사항) */}
{ - const updated = [...(config.parentDataMapping || [])]; - updated[index] = { ...updated[index], defaultValue: e.target.value }; - handleChange("parentDataMapping", updated); + const newValue = e.target.value; + setLocalMappingInputs(prev => ({ ...prev, [index]: newValue })); + }} + onBlur={() => { + const currentValue = localMappingInputs[index]; + if (currentValue !== undefined) { + const updated = [...(config.parentDataMapping || [])]; + updated[index] = { ...updated[index], defaultValue: currentValue || undefined }; + handleChange("parentDataMapping", updated); + } }} placeholder="값이 없을 때 사용할 기본값" className="h-7 text-xs" @@ -2307,46 +2621,24 @@ export const SelectedItemsDetailInputConfigPanel: React.FC {/* 삭제 버튼 */} - +
+ +
))} - - {(config.parentDataMapping || []).length === 0 && ( -

- 매핑 설정이 없습니다. "추가" 버튼을 클릭하여 설정하세요. -

- )} - - {/* 예시 */} -
-

💡 예시

-
-

매핑 1: 거래처 ID

-

• 소스 테이블: customer_mng

-

• 원본 필드: id → 저장 필드: customer_id

- -

매핑 2: 품목 ID

-

• 소스 테이블: item_info

-

• 원본 필드: id → 저장 필드: item_id

- -

매핑 3: 품목 기준단가

-

• 소스 테이블: item_info

-

• 원본 필드: standard_price → 저장 필드: base_price

-
-
{/* 사용 예시 */} @@ -2363,3 +2655,5 @@ export const SelectedItemsDetailInputConfigPanel: React.FC = ({ - ) : (() => { - console.log("🔍 [TableList] 렌더링 조건 체크", { - groupByColumns: groupByColumns.length, - groupedDataLength: groupedData.length, - willRenderGrouped: groupByColumns.length > 0 && groupedData.length > 0, - dataLength: data.length, - }); - return groupByColumns.length > 0 && groupedData.length > 0; - })() ? ( + ) : groupByColumns.length > 0 && groupedData.length > 0 ? ( // 그룹화된 렌더링 groupedData.map((group) => { - console.log("📊 [TableList] 그룹 렌더링:", group.groupKey, group.count); const isCollapsed = collapsedGroups.has(group.groupKey); return ( @@ -2508,10 +2499,7 @@ export const TableListComponent: React.FC = ({ }) ) : ( // 일반 렌더링 (그룹 없음) - (() => { - console.log("📋 [TableList] 일반 렌더링 시작:", data.length, "개 행"); - return data; - })().map((row, index) => ( + data.map((row, index) => ( )} - {/* 동적 모드일 때만 설정 버튼들 표시 */} + {/* 동적 모드일 때만 설정 버튼들 표시 (미리보기에서는 비활성화) */} {filterMode === "dynamic" && ( <>