Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
8d82baff0d
|
|
@ -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 <div style=\"font-family: Arial, sans-serif; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\">ㄴㅇㄹㄴㅇㄹㄴㅇㄹㅇ리'ㅐㅔ'ㅑ678463ㅎㄱ휼췇흍츄</p>\r\n </div>\r\n <br/><br/>\r\n <div style=\"border: 1px solid #ccc; padding: 15px; margin: 10px 0; background-color: #f9f9f9;\">\r\n <p><strong>---------- 전달된 메시지 ----------</strong></p>\r\n <p><strong>보낸 사람:</strong> \"이희진\" <zian9227@naver.com></p>\r\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 1:32:34</p>\r\n <p><strong>제목:</strong> ㅏㅣ</p>\r\n <hr style=\"border: none; border-top: 1px solid #ccc; margin: 10px 0;\" />\r\n undefined\r\n </div>\r\n ",
|
|
||||||
"status": "success",
|
|
||||||
"messageId": "<74dbd467-6185-024d-dd60-bf4459ff9ea4@wace.me>",
|
|
||||||
"accepted": [
|
|
||||||
"zian9227@naver.com"
|
|
||||||
],
|
|
||||||
"rejected": [],
|
|
||||||
"deletedAt": "2025-10-22T06:36:10.876Z"
|
|
||||||
}
|
|
||||||
|
|
@ -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보낸사람: \"이희진\" <zian9227@naver.com>\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"
|
|
||||||
}
|
|
||||||
|
|
@ -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": [
|
|
||||||
"\"이희진\" <zian9227@naver.com>"
|
|
||||||
],
|
|
||||||
"subject": "Re: ㅏㅣ",
|
|
||||||
"htmlContent": "\r\n <div style=\"font-family: Arial, sans-serif; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\">ㅁㄴㅇㄹㅁㅇㄴㄹㅁㄴㅇㄹㅁㄴㅇㄹㅁㄴㅇㄹㅁㄴㅇㄹㄴㅁㅇㄹ</p>\r\n </div>\r\n <br/><br/>\r\n <div style=\"border-left: 3px solid #ccc; padding-left: 10px; margin-left: 10px; color: #666;\">\r\n <p><strong>보낸 사람:</strong> \"이희진\" <zian9227@naver.com></p>\r\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 1:32:34</p>\r\n <p><strong>제목:</strong> ㅏㅣ</p>\r\n <hr style=\"border: none; border-top: 1px solid #ccc; margin: 10px 0;\" />\r\n undefined\r\n </div>\r\n ",
|
|
||||||
"status": "success",
|
|
||||||
"messageId": "<f085efa6-2668-0293-57de-88b1e7009dd1@wace.me>",
|
|
||||||
"accepted": [
|
|
||||||
"zian9227@naver.com"
|
|
||||||
],
|
|
||||||
"rejected": [],
|
|
||||||
"deletedAt": "2025-10-22T07:11:04.666Z"
|
|
||||||
}
|
|
||||||
|
|
@ -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 <br/><br/>\n <div style=\"border-left: 3px solid #ccc; padding-left: 15px; margin-top: 20px; color: #666;\">\n <p><strong>---------- 전달된 메일 ----------</strong></p>\n <p><strong>보낸사람:</strong> \"이희진\" <zian9227@naver.com></p>\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 12:58:15</p>\n <p><strong>제목:</strong> ㄴ</p>\n <hr style=\"border: none; border-top: 1px solid #ddd; margin: 10px 0;\"/>\n <p style=\"white-space: pre-wrap;\">ㄴㅇㄹㄴㅇㄹㄴㅇㄹ\n</p>\n </div>\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"
|
|
||||||
}
|
|
||||||
|
|
@ -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 <br/><br/>\n <div style=\"border-left: 3px solid #ccc; padding-left: 15px; margin-top: 20px; color: #666;\">\n <p><strong>원본 메일:</strong></p>\n <p><strong>보낸사람:</strong> \"이희진\" <zian9227@naver.com></p>\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 12:58:15</p>\n <p><strong>제목:</strong> ㄴ</p>\n <hr style=\"border: none; border-top: 1px solid #ddd; margin: 10px 0;\"/>\n <p style=\"white-space: pre-wrap;\">undefined</p>\n </div>\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"
|
|
||||||
}
|
|
||||||
|
|
@ -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 <br/><br/>\n <div style=\"border-left: 3px solid #ccc; padding-left: 15px; margin-top: 20px; color: #666;\">\n <p><strong>---------- 전달된 메일 ----------</strong></p>\n <p><strong>보낸사람:</strong> \"이희진\" <zian9227@naver.com></p>\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 12:58:15</p>\n <p><strong>제목:</strong> ㄴ</p>\n <hr style=\"border: none; border-top: 1px solid #ddd; margin: 10px 0;\"/>\n <p style=\"white-space: pre-wrap;\"></p>\n </div>\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"
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
@ -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 <br/><br/>\n <div style=\"border-left: 3px solid #ccc; padding-left: 15px; margin-top: 20px; color: #666;\">\n <p><strong>---------- 전달된 메일 ----------</strong></p>\n <p><strong>보낸사람:</strong> \"이희진\" <zian9227@naver.com></p>\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 12:58:15</p>\n <p><strong>제목:</strong> ㄴ</p>\n <hr style=\"border: none; border-top: 1px solid #ddd; margin: 10px 0;\"/>\n <p style=\"white-space: pre-wrap;\">ㄴㅇㄹㄴㅇㄹㄴㅇㄹ\n</p>\n </div>\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"
|
|
||||||
}
|
|
||||||
|
|
@ -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 <br/><br/>\n <div style=\"border-left: 3px solid #ccc; padding-left: 15px; margin-top: 20px; color: #666;\">\n <p><strong>원본 메일:</strong></p>\n <p><strong>보낸사람:</strong> \"이희진\" <zian9227@naver.com></p>\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 12:58:15</p>\n <p><strong>제목:</strong> ㄴ</p>\n <hr style=\"border: none; border-top: 1px solid #ddd; margin: 10px 0;\"/>\n <p style=\"white-space: pre-wrap;\">undefined</p>\n </div>\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"
|
|
||||||
}
|
|
||||||
|
|
@ -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 <br/><br/>\n <div style=\"border-left: 3px solid #ccc; padding-left: 15px; margin-top: 20px; color: #666;\">\n <p><strong>---------- 전달된 메일 ----------</strong></p>\n <p><strong>보낸사람:</strong> \"이희진\" <zian9227@naver.com></p>\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 1:32:34</p>\n <p><strong>제목:</strong> ㅏㅣ</p>\n <hr style=\"border: none; border-top: 1px solid #ddd; margin: 10px 0;\"/>\n undefined\n </div>\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"
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
@ -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": [
|
|
||||||
"\"이희진\" <zian9227@naver.com>"
|
|
||||||
],
|
|
||||||
"subject": "Re: ㅅㄷㄴㅅ",
|
|
||||||
"htmlContent": "\r\n <div style=\"font-family: Arial, sans-serif; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\">배불르고 졸린데 커피먹으니깐 졸린건 괜찮아졋고 배불러서 물배찼당아아아아</p>\r\n </div>\r\n <br/><br/>\r\n <div style=\"border-left: 3px solid #ccc; padding-left: 10px; margin-left: 10px; color: #666;\">\r\n <p><strong>보낸 사람:</strong> \"이희진\" <zian9227@naver.com></p>\r\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 1:03:03</p>\r\n <p><strong>제목:</strong> ㅅㄷㄴㅅ</p>\r\n <hr style=\"border: none; border-top: 1px solid #ccc; margin: 10px 0;\" />\r\n undefined\r\n </div>\r\n ",
|
|
||||||
"status": "success",
|
|
||||||
"messageId": "<0f215ba8-a1e4-8c5a-f43f-962f0717c161@wace.me>",
|
|
||||||
"accepted": [
|
|
||||||
"zian9227@naver.com"
|
|
||||||
],
|
|
||||||
"rejected": [],
|
|
||||||
"deletedAt": "2025-10-22T07:11:10.245Z"
|
|
||||||
}
|
|
||||||
|
|
@ -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 <br/><br/>\n <div style=\"border-left: 3px solid #ccc; padding-left: 15px; margin-top: 20px; color: #666;\">\n <p><strong>원본 메일:</strong></p>\n <p><strong>보낸사람:</strong> \"이희진\" <zian9227@naver.com></p>\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 1:32:34</p>\n <p><strong>제목:</strong> ㅏㅣ</p>\n <hr style=\"border: none; border-top: 1px solid #ddd; margin: 10px 0;\"/>\n <p style=\"white-space: pre-wrap;\">undefined</p>\n </div>\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"
|
|
||||||
}
|
|
||||||
|
|
@ -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 <br/><br/>\n <div style=\"border-left: 3px solid #ccc; padding-left: 15px; margin-top: 20px; color: #666;\">\n <p><strong>원본 메일:</strong></p>\n <p><strong>보낸사람:</strong> \"이희진\" <zian9227@naver.com></p>\n <p><strong>날짜:</strong> 2025. 10. 13. 오전 10:40:30</p>\n <p><strong>제목:</strong> 수신메일확인용</p>\n <hr style=\"border: none; border-top: 1px solid #ddd; margin: 10px 0;\"/>\n undefined\n </div>\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"
|
|
||||||
}
|
|
||||||
|
|
@ -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보낸사람: \"이희진\" <zian9227@naver.com>\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"
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
@ -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": [
|
|
||||||
"\"이희진\" <zian9227@naver.com>"
|
|
||||||
],
|
|
||||||
"subject": "Re: ㅅㄷㄴㅅ",
|
|
||||||
"htmlContent": "\r\n <div style=\"font-family: Arial, sans-serif; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\">ㅁㄴㅇㄹㅁㄴㅇㄹㅁㄴㅇㄹㅁㄴㅇㄹㄴㅇㄹㄴㅇㄹ</p>\r\n </div>\r\n <br/><br/>\r\n <div style=\"border-left: 3px solid #ccc; padding-left: 10px; margin-left: 10px; color: #666;\">\r\n <p><strong>보낸 사람:</strong> \"이희진\" <zian9227@naver.com></p>\r\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 1:03:03</p>\r\n <p><strong>제목:</strong> ㅅㄷㄴㅅ</p>\r\n <hr style=\"border: none; border-top: 1px solid #ccc; margin: 10px 0;\" />\r\n undefined\r\n </div>\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": "<e68a0501-f79a-8713-a625-e882f711b30d@wace.me>",
|
|
||||||
"accepted": [
|
|
||||||
"zian9227@naver.com"
|
|
||||||
],
|
|
||||||
"rejected": [],
|
|
||||||
"deletedAt": "2025-10-22T07:11:12.907Z"
|
|
||||||
}
|
|
||||||
|
|
@ -99,10 +99,18 @@ export class DynamicFormService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// YYYY-MM-DD 형식인 경우 시간 추가해서 Date 객체 생성
|
// YYYY-MM-DD 형식인 경우
|
||||||
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||||
console.log(`📅 날짜 타입 변환: ${value} -> Date 객체`);
|
// DATE 타입이면 문자열 그대로 유지
|
||||||
return new Date(value + "T00:00:00");
|
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 객체로 변환
|
// 다른 날짜 형식도 Date 객체로 변환
|
||||||
else {
|
else {
|
||||||
|
|
@ -300,13 +308,13 @@ export class DynamicFormService {
|
||||||
) {
|
) {
|
||||||
// YYYY-MM-DD HH:mm:ss 형태의 문자열을 Date 객체로 변환
|
// YYYY-MM-DD HH:mm:ss 형태의 문자열을 Date 객체로 변환
|
||||||
if (value.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)) {
|
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);
|
dataToInsert[key] = new Date(value);
|
||||||
}
|
}
|
||||||
// YYYY-MM-DD 형태의 문자열을 Date 객체로 변환
|
// YYYY-MM-DD 형태의 문자열은 그대로 유지 (DATE 타입으로 저장)
|
||||||
else if (value.match(/^\d{4}-\d{2}-\d{2}$/)) {
|
else if (value.match(/^\d{4}-\d{2}-\d{2}$/)) {
|
||||||
console.log(`📅 날짜 변환: ${key} = "${value}" -> Date 객체`);
|
console.log(`📅 날짜 유지: ${key} = "${value}" -> 문자열 그대로 (DATE 타입)`);
|
||||||
dataToInsert[key] = new Date(value + "T00:00:00");
|
// dataToInsert[key] = value; // 문자열 그대로 유지 (이미 올바른 형식)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -849,10 +857,22 @@ export class DynamicFormService {
|
||||||
const values: any[] = Object.values(changedFields);
|
const values: any[] = Object.values(changedFields);
|
||||||
values.push(id); // WHERE 조건용 ID 추가
|
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 = `
|
const updateQuery = `
|
||||||
UPDATE ${tableName}
|
UPDATE ${tableName}
|
||||||
SET ${setClause}
|
SET ${setClause}
|
||||||
WHERE ${primaryKeyColumn} = $${values.length}::text
|
WHERE ${primaryKeyColumn} = $${values.length}${pkCast}
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -356,17 +356,6 @@ function ScreenViewPage() {
|
||||||
return isButton;
|
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) => {
|
topLevelComponents.forEach((component) => {
|
||||||
const isButton =
|
const isButton =
|
||||||
|
|
@ -406,33 +395,13 @@ function ScreenViewPage() {
|
||||||
(c) => (c as any).componentId === "table-search-widget",
|
(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(
|
const conditionalContainers = regularComponents.filter(
|
||||||
(c) =>
|
(c) =>
|
||||||
(c as any).componentId === "conditional-container" ||
|
(c as any).componentId === "conditional-container" ||
|
||||||
(c as any).componentType === "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 위치 조정
|
// TableSearchWidget 및 조건부 컨테이너 높이 차이를 계산하여 Y 위치 조정
|
||||||
const adjustedComponents = regularComponents.map((component) => {
|
const adjustedComponents = regularComponents.map((component) => {
|
||||||
const isTableSearchWidget = (component as any).componentId === "table-search-widget";
|
const isTableSearchWidget = (component as any).componentId === "table-search-widget";
|
||||||
|
|
@ -520,12 +489,6 @@ function ScreenViewPage() {
|
||||||
columnOrder={tableColumnOrder}
|
columnOrder={tableColumnOrder}
|
||||||
tableDisplayData={tableDisplayData}
|
tableDisplayData={tableDisplayData}
|
||||||
onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder, 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);
|
setSelectedRowsData(selectedData);
|
||||||
setTableSortBy(sortBy);
|
setTableSortBy(sortBy);
|
||||||
setTableSortOrder(sortOrder || "asc");
|
setTableSortOrder(sortOrder || "asc");
|
||||||
|
|
@ -604,12 +567,6 @@ function ScreenViewPage() {
|
||||||
columnOrder,
|
columnOrder,
|
||||||
tableDisplayData,
|
tableDisplayData,
|
||||||
) => {
|
) => {
|
||||||
console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData);
|
|
||||||
console.log("📊 정렬 정보 (자식):", { sortBy, sortOrder, columnOrder });
|
|
||||||
console.log("📊 화면 표시 데이터 (자식):", {
|
|
||||||
count: tableDisplayData?.length,
|
|
||||||
firstRow: tableDisplayData?.[0],
|
|
||||||
});
|
|
||||||
setSelectedRowsData(selectedData);
|
setSelectedRowsData(selectedData);
|
||||||
setTableSortBy(sortBy);
|
setTableSortBy(sortBy);
|
||||||
setTableSortOrder(sortOrder || "asc");
|
setTableSortOrder(sortOrder || "asc");
|
||||||
|
|
@ -618,7 +575,6 @@ function ScreenViewPage() {
|
||||||
}}
|
}}
|
||||||
refreshKey={tableRefreshKey}
|
refreshKey={tableRefreshKey}
|
||||||
onRefresh={() => {
|
onRefresh={() => {
|
||||||
console.log("🔄 테이블 새로고침 요청됨 (자식)");
|
|
||||||
setTableRefreshKey((prev) => prev + 1);
|
setTableRefreshKey((prev) => prev + 1);
|
||||||
setSelectedRowsData([]); // 선택 해제
|
setSelectedRowsData([]); // 선택 해제
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,9 @@ export function OrderRegistrationModal({
|
||||||
// 선택된 품목 목록
|
// 선택된 품목 목록
|
||||||
const [selectedItems, setSelectedItems] = useState<any[]>([]);
|
const [selectedItems, setSelectedItems] = useState<any[]>([]);
|
||||||
|
|
||||||
|
// 납기일 일괄 적용 플래그 (딱 한 번만 실행)
|
||||||
|
const [isDeliveryDateApplied, setIsDeliveryDateApplied] = useState(false);
|
||||||
|
|
||||||
// 저장 중
|
// 저장 중
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
|
@ -158,6 +161,45 @@ export function OrderRegistrationModal({
|
||||||
hsCode: "",
|
hsCode: "",
|
||||||
});
|
});
|
||||||
setSelectedItems([]);
|
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({
|
||||||
<Label className="text-xs sm:text-sm">추가된 품목</Label>
|
<Label className="text-xs sm:text-sm">추가된 품목</Label>
|
||||||
<OrderItemRepeaterTable
|
<OrderItemRepeaterTable
|
||||||
value={selectedItems}
|
value={selectedItems}
|
||||||
onChange={setSelectedItems}
|
onChange={handleItemsChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,33 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||||
screenId: modalState.screenId,
|
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 insertedCount = 0;
|
||||||
let updatedCount = 0;
|
let updatedCount = 0;
|
||||||
let deletedCount = 0;
|
let deletedCount = 0;
|
||||||
|
|
@ -333,6 +360,17 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||||
|
|
||||||
delete insertData.id; // id는 자동 생성되므로 제거
|
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 등)
|
// 🆕 groupByColumns의 값을 강제로 포함 (order_no 등)
|
||||||
if (modalState.groupByColumns && modalState.groupByColumns.length > 0) {
|
if (modalState.groupByColumns && modalState.groupByColumns.length > 0) {
|
||||||
modalState.groupByColumns.forEach((colName) => {
|
modalState.groupByColumns.forEach((colName) => {
|
||||||
|
|
@ -348,23 +386,32 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||||
// 🆕 공통 필드 추가 (거래처, 담당자, 납품처, 메모 등)
|
// 🆕 공통 필드 추가 (거래처, 담당자, 납품처, 메모 등)
|
||||||
// formData에서 품목별 필드가 아닌 공통 필드를 복사
|
// formData에서 품목별 필드가 아닌 공통 필드를 복사
|
||||||
const commonFields = [
|
const commonFields = [
|
||||||
'partner_id', // 거래처
|
"partner_id", // 거래처
|
||||||
'manager_id', // 담당자
|
"manager_id", // 담당자
|
||||||
'delivery_partner_id', // 납품처
|
"delivery_partner_id", // 납품처
|
||||||
'delivery_address', // 납품장소
|
"delivery_address", // 납품장소
|
||||||
'memo', // 메모
|
"memo", // 메모
|
||||||
'order_date', // 주문일
|
"order_date", // 주문일
|
||||||
'due_date', // 납기일
|
"due_date", // 납기일
|
||||||
'shipping_method', // 배송방법
|
"shipping_method", // 배송방법
|
||||||
'status', // 상태
|
"status", // 상태
|
||||||
'sales_type', // 영업유형
|
"sales_type", // 영업유형
|
||||||
];
|
];
|
||||||
|
|
||||||
commonFields.forEach((fieldName) => {
|
commonFields.forEach((fieldName) => {
|
||||||
// formData에 값이 있으면 추가
|
// formData에 값이 있으면 추가
|
||||||
if (formData[fieldName] !== undefined && formData[fieldName] !== null) {
|
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<EditModalProps> = ({ className }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 값 정규화 함수 (타입 통일)
|
// 🆕 값 정규화 함수 (타입 통일)
|
||||||
const normalizeValue = (val: any): any => {
|
const normalizeValue = (val: any, fieldName?: string): any => {
|
||||||
if (val === null || val === undefined || val === "") return null;
|
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))) {
|
if (typeof val === "string" && !isNaN(Number(val))) {
|
||||||
// 숫자로 변환 가능한 문자열은 숫자로
|
// 숫자로 변환 가능한 문자열은 숫자로
|
||||||
return Number(val);
|
return Number(val);
|
||||||
|
|
@ -422,13 +476,14 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 타입 정규화 후 비교
|
// 🆕 타입 정규화 후 비교
|
||||||
const currentValue = normalizeValue(currentData[key]);
|
const currentValue = normalizeValue(currentData[key], key);
|
||||||
const originalValue = normalizeValue(originalItemData[key]);
|
const originalValue = normalizeValue(originalItemData[key], key);
|
||||||
|
|
||||||
// 값이 변경된 경우만 포함
|
// 값이 변경된 경우만 포함
|
||||||
if (currentValue !== originalValue) {
|
if (currentValue !== originalValue) {
|
||||||
console.log(`🔍 [품목 수정 감지] ${key}: ${originalValue} → ${currentValue}`);
|
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<EditModalProps> = ({ className }) => {
|
||||||
maxHeight: "100%",
|
maxHeight: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 🆕 그룹 데이터가 있으면 안내 메시지 표시 */}
|
|
||||||
{groupData.length > 1 && (
|
|
||||||
<div className="absolute left-4 top-4 z-10 rounded-md bg-blue-50 px-3 py-2 text-xs text-blue-700 shadow-sm">
|
|
||||||
{groupData.length}개의 관련 품목을 함께 수정합니다
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{screenData.components.map((component) => {
|
{screenData.components.map((component) => {
|
||||||
// 컴포넌트 위치를 offset만큼 조정
|
// 컴포넌트 위치를 offset만큼 조정
|
||||||
const offsetX = screenDimensions?.offsetX || 0;
|
const offsetX = screenDimensions?.offsetX || 0;
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,9 @@ import dynamic from "next/dynamic";
|
||||||
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
||||||
import { DynamicWebTypeRenderer } from "@/lib/registry";
|
import { DynamicWebTypeRenderer } from "@/lib/registry";
|
||||||
import { isFileComponent, getComponentWebType } from "@/lib/utils/componentTypeUtils";
|
import { isFileComponent, getComponentWebType } from "@/lib/utils/componentTypeUtils";
|
||||||
|
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
||||||
|
import { RealtimePreview } from "./RealtimePreviewDynamic";
|
||||||
|
import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext";
|
||||||
|
|
||||||
// InteractiveScreenViewer를 동적으로 import (SSR 비활성화)
|
// InteractiveScreenViewer를 동적으로 import (SSR 비활성화)
|
||||||
const InteractiveScreenViewer = dynamic(
|
const InteractiveScreenViewer = dynamic(
|
||||||
|
|
@ -1315,24 +1318,40 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>화면 미리보기 - {screenToPreview?.screenName}</DialogTitle>
|
<DialogTitle>화면 미리보기 - {screenToPreview?.screenName}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex flex-1 items-center justify-center overflow-hidden bg-gradient-to-br from-gray-50 to-slate-100 p-6">
|
<ScreenPreviewProvider isPreviewMode={true}>
|
||||||
{isLoadingPreview ? (
|
<TableOptionsProvider>
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex flex-1 items-center justify-center overflow-hidden bg-gradient-to-br from-gray-50 to-slate-100 p-6">
|
||||||
<div className="text-center">
|
{isLoadingPreview ? (
|
||||||
<div className="mb-2 text-lg font-medium">레이아웃 로딩 중...</div>
|
<div className="flex h-full items-center justify-center">
|
||||||
<div className="text-muted-foreground text-sm">화면 정보를 불러오고 있습니다.</div>
|
<div className="text-center">
|
||||||
|
<div className="mb-2 text-lg font-medium">레이아웃 로딩 중...</div>
|
||||||
|
<div className="text-muted-foreground text-sm">화면 정보를 불러오고 있습니다.</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : previewLayout && previewLayout.components ? (
|
||||||
) : previewLayout && previewLayout.components ? (
|
|
||||||
(() => {
|
(() => {
|
||||||
const screenWidth = previewLayout.screenResolution?.width || 1200;
|
const screenWidth = previewLayout.screenResolution?.width || 1200;
|
||||||
const screenHeight = previewLayout.screenResolution?.height || 800;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -1414,115 +1433,61 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 라벨 표시 여부 계산
|
// 일반 컴포넌트 렌더링 - RealtimePreview 사용 (실제 화면과 동일)
|
||||||
const templateTypes = ["datatable"];
|
|
||||||
const shouldShowLabel =
|
|
||||||
component.style?.labelDisplay !== false &&
|
|
||||||
(component.label || component.style?.labelText) &&
|
|
||||||
!templateTypes.includes(component.type);
|
|
||||||
|
|
||||||
const labelText = component.style?.labelText || component.label || "";
|
|
||||||
const labelStyle = {
|
|
||||||
fontSize: component.style?.labelFontSize || "14px",
|
|
||||||
color: component.style?.labelColor || "#212121",
|
|
||||||
fontWeight: component.style?.labelFontWeight || "500",
|
|
||||||
backgroundColor: component.style?.labelBackgroundColor || "transparent",
|
|
||||||
};
|
|
||||||
const labelMarginBottom = component.style?.labelMarginBottom || "4px";
|
|
||||||
|
|
||||||
// 일반 컴포넌트 렌더링
|
|
||||||
return (
|
return (
|
||||||
<div key={component.id}>
|
<RealtimePreview
|
||||||
{/* 라벨을 외부에 별도로 렌더링 */}
|
key={component.id}
|
||||||
{shouldShowLabel && (
|
component={component}
|
||||||
<div
|
isSelected={false}
|
||||||
style={{
|
isDesignMode={false}
|
||||||
position: "absolute",
|
onClick={() => {}}
|
||||||
left: `${component.position.x}px`,
|
screenId={screenToPreview!.screenId}
|
||||||
top: `${component.position.y - 25}px`, // 컴포넌트 위쪽에 라벨 배치
|
tableName={screenToPreview?.tableName}
|
||||||
zIndex: (component.position.z || 1) + 1,
|
formData={previewFormData}
|
||||||
...labelStyle,
|
onFormDataChange={(fieldName, value) => {
|
||||||
}}
|
setPreviewFormData((prev) => ({
|
||||||
>
|
...prev,
|
||||||
{labelText}
|
[fieldName]: value,
|
||||||
{component.required && <span style={{ color: "#f97316", marginLeft: "2px" }}>*</span>}
|
}));
|
||||||
</div>
|
}}
|
||||||
)}
|
>
|
||||||
|
{/* 자식 컴포넌트들 */}
|
||||||
|
{(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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
{/* 실제 컴포넌트 */}
|
return (
|
||||||
<div
|
<RealtimePreview
|
||||||
style={(() => {
|
key={child.id}
|
||||||
const style = {
|
component={relativeChildComponent}
|
||||||
position: "absolute" as const,
|
isSelected={false}
|
||||||
left: `${component.position.x}px`,
|
isDesignMode={false}
|
||||||
top: `${component.position.y}px`,
|
onClick={() => {}}
|
||||||
width: component.style?.width || `${component.size.width}px`,
|
screenId={screenToPreview!.screenId}
|
||||||
height: component.style?.height || `${component.size.height}px`,
|
tableName={screenToPreview?.tableName}
|
||||||
zIndex: component.position.z || 1,
|
formData={previewFormData}
|
||||||
};
|
onFormDataChange={(fieldName, value) => {
|
||||||
|
setPreviewFormData((prev) => ({
|
||||||
return style;
|
...prev,
|
||||||
})()}
|
[fieldName]: value,
|
||||||
>
|
}));
|
||||||
{/* 위젯 컴포넌트가 아닌 경우 DynamicComponentRenderer 사용 */}
|
}}
|
||||||
{component.type !== "widget" ? (
|
/>
|
||||||
<DynamicComponentRenderer
|
);
|
||||||
component={{
|
})}
|
||||||
...component,
|
</RealtimePreview>
|
||||||
style: {
|
|
||||||
...component.style,
|
|
||||||
labelDisplay: shouldShowLabel ? false : (component.style?.labelDisplay ?? true), // 상위에서 라벨을 표시했으면 컴포넌트 내부에서는 숨김
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
isInteractive={true}
|
|
||||||
formData={previewFormData}
|
|
||||||
onFormDataChange={(fieldName, value) => {
|
|
||||||
setPreviewFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[fieldName]: value,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
screenId={screenToPreview!.screenId}
|
|
||||||
tableName={screenToPreview?.tableName}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<DynamicWebTypeRenderer
|
|
||||||
webType={(() => {
|
|
||||||
// 유틸리티 함수로 파일 컴포넌트 감지
|
|
||||||
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",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1536,7 +1501,9 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</TableOptionsProvider>
|
||||||
|
</ScreenPreviewProvider>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setPreviewDialogOpen(false)}>
|
<Button variant="outline" onClick={() => setPreviewDialogOpen(false)}>
|
||||||
닫기
|
닫기
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,14 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
// 새 옵션 추가용 상태
|
// 새 옵션 추가용 상태
|
||||||
const [newOptionLabel, setNewOptionLabel] = useState("");
|
const [newOptionLabel, setNewOptionLabel] = useState("");
|
||||||
const [newOptionValue, setNewOptionValue] = useState("");
|
const [newOptionValue, setNewOptionValue] = useState("");
|
||||||
|
|
||||||
|
// 입력 필드용 로컬 상태
|
||||||
|
const [localInputs, setLocalInputs] = useState({
|
||||||
|
label: config.label || "",
|
||||||
|
checkedValue: config.checkedValue || "Y",
|
||||||
|
uncheckedValue: config.uncheckedValue || "N",
|
||||||
|
groupLabel: config.groupLabel || "",
|
||||||
|
});
|
||||||
|
|
||||||
// 컴포넌트 변경 시 로컬 상태 동기화
|
// 컴포넌트 변경 시 로컬 상태 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -63,6 +71,14 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
readonly: currentConfig.readonly || false,
|
readonly: currentConfig.readonly || false,
|
||||||
inline: currentConfig.inline !== false,
|
inline: currentConfig.inline !== false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 입력 필드 로컬 상태도 동기화
|
||||||
|
setLocalInputs({
|
||||||
|
label: currentConfig.label || "",
|
||||||
|
checkedValue: currentConfig.checkedValue || "Y",
|
||||||
|
uncheckedValue: currentConfig.uncheckedValue || "N",
|
||||||
|
groupLabel: currentConfig.groupLabel || "",
|
||||||
|
});
|
||||||
}, [widget.webTypeConfig]);
|
}, [widget.webTypeConfig]);
|
||||||
|
|
||||||
// 설정 업데이트 핸들러
|
// 설정 업데이트 핸들러
|
||||||
|
|
@ -107,11 +123,16 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
updateConfig("options", newOptions);
|
updateConfig("options", newOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 옵션 업데이트
|
// 옵션 업데이트 (입력 필드용 - 로컬 상태만)
|
||||||
const updateOption = (index: number, field: keyof CheckboxOption, value: any) => {
|
const updateOptionLocal = (index: number, field: keyof CheckboxOption, value: any) => {
|
||||||
const newOptions = [...localConfig.options];
|
const newOptions = [...localConfig.options];
|
||||||
newOptions[index] = { ...newOptions[index], [field]: value };
|
newOptions[index] = { ...newOptions[index], [field]: value };
|
||||||
updateConfig("options", newOptions);
|
setLocalConfig({ ...localConfig, options: newOptions });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 옵션 업데이트 완료 (onBlur)
|
||||||
|
const handleOptionBlur = () => {
|
||||||
|
onUpdateProperty("webTypeConfig", localConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -170,8 +191,9 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="label"
|
id="label"
|
||||||
value={localConfig.label || ""}
|
value={localInputs.label}
|
||||||
onChange={(e) => updateConfig("label", e.target.value)}
|
onChange={(e) => setLocalInputs({ ...localInputs, label: e.target.value })}
|
||||||
|
onBlur={() => updateConfig("label", localInputs.label)}
|
||||||
placeholder="체크박스 라벨"
|
placeholder="체크박스 라벨"
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
|
|
@ -184,8 +206,9 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="checkedValue"
|
id="checkedValue"
|
||||||
value={localConfig.checkedValue || ""}
|
value={localInputs.checkedValue}
|
||||||
onChange={(e) => updateConfig("checkedValue", e.target.value)}
|
onChange={(e) => setLocalInputs({ ...localInputs, checkedValue: e.target.value })}
|
||||||
|
onBlur={() => updateConfig("checkedValue", localInputs.checkedValue)}
|
||||||
placeholder="Y"
|
placeholder="Y"
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
|
|
@ -196,8 +219,9 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="uncheckedValue"
|
id="uncheckedValue"
|
||||||
value={localConfig.uncheckedValue || ""}
|
value={localInputs.uncheckedValue}
|
||||||
onChange={(e) => updateConfig("uncheckedValue", e.target.value)}
|
onChange={(e) => setLocalInputs({ ...localInputs, uncheckedValue: e.target.value })}
|
||||||
|
onBlur={() => updateConfig("uncheckedValue", localInputs.uncheckedValue)}
|
||||||
placeholder="N"
|
placeholder="N"
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
|
|
@ -229,8 +253,9 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="groupLabel"
|
id="groupLabel"
|
||||||
value={localConfig.groupLabel || ""}
|
value={localInputs.groupLabel}
|
||||||
onChange={(e) => updateConfig("groupLabel", e.target.value)}
|
onChange={(e) => setLocalInputs({ ...localInputs, groupLabel: e.target.value })}
|
||||||
|
onBlur={() => updateConfig("groupLabel", localInputs.groupLabel)}
|
||||||
placeholder="체크박스 그룹 제목"
|
placeholder="체크박스 그룹 제목"
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
|
|
@ -268,26 +293,40 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
<Label className="text-xs">옵션 목록 ({localConfig.options.length}개)</Label>
|
<Label className="text-xs">옵션 목록 ({localConfig.options.length}개)</Label>
|
||||||
<div className="max-h-40 space-y-2 overflow-y-auto">
|
<div className="max-h-40 space-y-2 overflow-y-auto">
|
||||||
{localConfig.options.map((option, index) => (
|
{localConfig.options.map((option, index) => (
|
||||||
<div key={index} className="flex items-center gap-2 rounded border p-2">
|
<div key={`${option.value}-${index}`} className="flex items-center gap-2 rounded border p-2">
|
||||||
<Switch
|
<Switch
|
||||||
checked={option.checked || false}
|
checked={option.checked || false}
|
||||||
onCheckedChange={(checked) => updateOption(index, "checked", checked)}
|
onCheckedChange={(checked) => {
|
||||||
|
const newOptions = [...localConfig.options];
|
||||||
|
newOptions[index] = { ...newOptions[index], checked };
|
||||||
|
const newConfig = { ...localConfig, options: newOptions };
|
||||||
|
setLocalConfig(newConfig);
|
||||||
|
onUpdateProperty("webTypeConfig", newConfig);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={option.label}
|
value={option.label}
|
||||||
onChange={(e) => updateOption(index, "label", e.target.value)}
|
onChange={(e) => updateOptionLocal(index, "label", e.target.value)}
|
||||||
|
onBlur={handleOptionBlur}
|
||||||
placeholder="라벨"
|
placeholder="라벨"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={option.value}
|
value={option.value}
|
||||||
onChange={(e) => updateOption(index, "value", e.target.value)}
|
onChange={(e) => updateOptionLocal(index, "value", e.target.value)}
|
||||||
|
onBlur={handleOptionBlur}
|
||||||
placeholder="값"
|
placeholder="값"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
checked={!option.disabled}
|
checked={!option.disabled}
|
||||||
onCheckedChange={(checked) => updateOption(index, "disabled", !checked)}
|
onCheckedChange={(checked) => {
|
||||||
|
const newOptions = [...localConfig.options];
|
||||||
|
newOptions[index] = { ...newOptions[index], disabled: !checked };
|
||||||
|
const newConfig = { ...localConfig, options: newOptions };
|
||||||
|
setLocalConfig(newConfig);
|
||||||
|
onUpdateProperty("webTypeConfig", newConfig);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Button size="sm" variant="destructive" onClick={() => removeOption(index)} className="p-1 text-xs">
|
<Button size="sm" variant="destructive" onClick={() => removeOption(index)} className="p-1 text-xs">
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
|
|
|
||||||
|
|
@ -51,32 +51,29 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
const [newFieldName, setNewFieldName] = useState("");
|
const [newFieldName, setNewFieldName] = useState("");
|
||||||
const [newFieldLabel, setNewFieldLabel] = useState("");
|
const [newFieldLabel, setNewFieldLabel] = useState("");
|
||||||
const [newFieldType, setNewFieldType] = useState("string");
|
const [newFieldType, setNewFieldType] = useState("string");
|
||||||
const [isUserEditing, setIsUserEditing] = useState(false);
|
|
||||||
|
|
||||||
// 컴포넌트 변경 시 로컬 상태 동기화 (사용자가 입력 중이 아닐 때만)
|
// 컴포넌트 변경 시 로컬 상태 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isUserEditing) {
|
const currentConfig = (widget.webTypeConfig as EntityTypeConfig) || {};
|
||||||
const currentConfig = (widget.webTypeConfig as EntityTypeConfig) || {};
|
setLocalConfig({
|
||||||
setLocalConfig({
|
entityType: currentConfig.entityType || "",
|
||||||
entityType: currentConfig.entityType || "",
|
displayFields: currentConfig.displayFields || [],
|
||||||
displayFields: currentConfig.displayFields || [],
|
searchFields: currentConfig.searchFields || [],
|
||||||
searchFields: currentConfig.searchFields || [],
|
valueField: currentConfig.valueField || "id",
|
||||||
valueField: currentConfig.valueField || "id",
|
labelField: currentConfig.labelField || "name",
|
||||||
labelField: currentConfig.labelField || "name",
|
multiple: currentConfig.multiple || false,
|
||||||
multiple: currentConfig.multiple || false,
|
searchable: currentConfig.searchable !== false,
|
||||||
searchable: currentConfig.searchable !== false,
|
placeholder: currentConfig.placeholder || "엔티티를 선택하세요",
|
||||||
placeholder: currentConfig.placeholder || "엔티티를 선택하세요",
|
emptyMessage: currentConfig.emptyMessage || "검색 결과가 없습니다",
|
||||||
emptyMessage: currentConfig.emptyMessage || "검색 결과가 없습니다",
|
pageSize: currentConfig.pageSize || 20,
|
||||||
pageSize: currentConfig.pageSize || 20,
|
minSearchLength: currentConfig.minSearchLength || 1,
|
||||||
minSearchLength: currentConfig.minSearchLength || 1,
|
defaultValue: currentConfig.defaultValue || "",
|
||||||
defaultValue: currentConfig.defaultValue || "",
|
required: currentConfig.required || false,
|
||||||
required: currentConfig.required || false,
|
readonly: currentConfig.readonly || false,
|
||||||
readonly: currentConfig.readonly || false,
|
apiEndpoint: currentConfig.apiEndpoint || "",
|
||||||
apiEndpoint: currentConfig.apiEndpoint || "",
|
filters: currentConfig.filters || {},
|
||||||
filters: currentConfig.filters || {},
|
});
|
||||||
});
|
}, [widget.webTypeConfig]);
|
||||||
}
|
|
||||||
}, [widget.webTypeConfig, isUserEditing]);
|
|
||||||
|
|
||||||
// 설정 업데이트 핸들러 (즉시 부모에게 전달 - 드롭다운, 체크박스 등)
|
// 설정 업데이트 핸들러 (즉시 부모에게 전달 - 드롭다운, 체크박스 등)
|
||||||
const updateConfig = (field: keyof EntityTypeConfig, value: any) => {
|
const updateConfig = (field: keyof EntityTypeConfig, value: any) => {
|
||||||
|
|
@ -87,13 +84,11 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
|
|
||||||
// 입력 필드용 업데이트 (로컬 상태만)
|
// 입력 필드용 업데이트 (로컬 상태만)
|
||||||
const updateConfigLocal = (field: keyof EntityTypeConfig, value: any) => {
|
const updateConfigLocal = (field: keyof EntityTypeConfig, value: any) => {
|
||||||
setIsUserEditing(true);
|
|
||||||
setLocalConfig({ ...localConfig, [field]: value });
|
setLocalConfig({ ...localConfig, [field]: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
// 입력 완료 시 부모에게 전달
|
// 입력 완료 시 부모에게 전달
|
||||||
const handleInputBlur = () => {
|
const handleInputBlur = () => {
|
||||||
setIsUserEditing(false);
|
|
||||||
onUpdateProperty("webTypeConfig", localConfig);
|
onUpdateProperty("webTypeConfig", localConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -121,17 +116,15 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
updateConfig("displayFields", newFields);
|
updateConfig("displayFields", newFields);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 필드 업데이트 (입력 중)
|
// 필드 업데이트 (입력 중) - 로컬 상태만 업데이트
|
||||||
const updateDisplayField = (index: number, field: keyof EntityField, value: any) => {
|
const updateDisplayField = (index: number, field: keyof EntityField, value: any) => {
|
||||||
setIsUserEditing(true);
|
|
||||||
const newFields = [...localConfig.displayFields];
|
const newFields = [...localConfig.displayFields];
|
||||||
newFields[index] = { ...newFields[index], [field]: value };
|
newFields[index] = { ...newFields[index], [field]: value };
|
||||||
setLocalConfig({ ...localConfig, displayFields: newFields });
|
setLocalConfig({ ...localConfig, displayFields: newFields });
|
||||||
};
|
};
|
||||||
|
|
||||||
// 필드 업데이트 완료 (onBlur)
|
// 필드 업데이트 완료 (onBlur) - 부모에게 전달
|
||||||
const handleFieldBlur = () => {
|
const handleFieldBlur = () => {
|
||||||
setIsUserEditing(false);
|
|
||||||
onUpdateProperty("webTypeConfig", localConfig);
|
onUpdateProperty("webTypeConfig", localConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -325,12 +318,15 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
<Label className="text-xs">표시 필드 ({localConfig.displayFields.length}개)</Label>
|
<Label className="text-xs">표시 필드 ({localConfig.displayFields.length}개)</Label>
|
||||||
<div className="max-h-40 space-y-2 overflow-y-auto">
|
<div className="max-h-40 space-y-2 overflow-y-auto">
|
||||||
{localConfig.displayFields.map((field, index) => (
|
{localConfig.displayFields.map((field, index) => (
|
||||||
<div key={index} className="flex items-center gap-2 rounded border p-2">
|
<div key={`${field.name}-${index}`} className="flex items-center gap-2 rounded border p-2">
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.visible}
|
checked={field.visible}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
updateDisplayField(index, "visible", checked);
|
const newFields = [...localConfig.displayFields];
|
||||||
handleFieldBlur();
|
newFields[index] = { ...newFields[index], visible: checked };
|
||||||
|
const newConfig = { ...localConfig, displayFields: newFields };
|
||||||
|
setLocalConfig(newConfig);
|
||||||
|
onUpdateProperty("webTypeConfig", newConfig);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -347,7 +343,16 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
placeholder="라벨"
|
placeholder="라벨"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<Select value={field.type} onValueChange={(value) => updateDisplayField(index, "type", value)}>
|
<Select
|
||||||
|
value={field.type}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newFields = [...localConfig.displayFields];
|
||||||
|
newFields[index] = { ...newFields[index], type: value };
|
||||||
|
const newConfig = { ...localConfig, displayFields: newFields };
|
||||||
|
setLocalConfig(newConfig);
|
||||||
|
onUpdateProperty("webTypeConfig", newConfig);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<SelectTrigger className="w-24 text-xs">
|
<SelectTrigger className="w-24 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,12 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
const [newOptionLabel, setNewOptionLabel] = useState("");
|
const [newOptionLabel, setNewOptionLabel] = useState("");
|
||||||
const [newOptionValue, setNewOptionValue] = useState("");
|
const [newOptionValue, setNewOptionValue] = useState("");
|
||||||
const [bulkOptions, setBulkOptions] = useState("");
|
const [bulkOptions, setBulkOptions] = useState("");
|
||||||
|
|
||||||
|
// 입력 필드용 로컬 상태
|
||||||
|
const [localInputs, setLocalInputs] = useState({
|
||||||
|
groupLabel: config.groupLabel || "",
|
||||||
|
groupName: config.groupName || "",
|
||||||
|
});
|
||||||
|
|
||||||
// 컴포넌트 변경 시 로컬 상태 동기화
|
// 컴포넌트 변경 시 로컬 상태 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -59,6 +65,12 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
inline: currentConfig.inline !== false,
|
inline: currentConfig.inline !== false,
|
||||||
groupLabel: currentConfig.groupLabel || "",
|
groupLabel: currentConfig.groupLabel || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 입력 필드 로컬 상태도 동기화
|
||||||
|
setLocalInputs({
|
||||||
|
groupLabel: currentConfig.groupLabel || "",
|
||||||
|
groupName: currentConfig.groupName || "",
|
||||||
|
});
|
||||||
}, [widget.webTypeConfig]);
|
}, [widget.webTypeConfig]);
|
||||||
|
|
||||||
// 설정 업데이트 핸들러
|
// 설정 업데이트 핸들러
|
||||||
|
|
@ -95,17 +107,24 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 옵션 업데이트
|
// 옵션 업데이트 (입력 필드용 - 로컬 상태만)
|
||||||
const updateOption = (index: number, field: keyof RadioOption, value: any) => {
|
const updateOptionLocal = (index: number, field: keyof RadioOption, value: any) => {
|
||||||
const newOptions = [...localConfig.options];
|
const newOptions = [...localConfig.options];
|
||||||
const oldValue = newOptions[index].value;
|
const oldValue = newOptions[index].value;
|
||||||
newOptions[index] = { ...newOptions[index], [field]: value };
|
newOptions[index] = { ...newOptions[index], [field]: value };
|
||||||
updateConfig("options", newOptions);
|
|
||||||
|
|
||||||
// 값이 변경되고 해당 값이 기본값이었다면 기본값도 업데이트
|
// 값이 변경되고 해당 값이 기본값이었다면 기본값도 업데이트
|
||||||
|
const newConfig = { ...localConfig, options: newOptions };
|
||||||
if (field === "value" && localConfig.defaultValue === oldValue) {
|
if (field === "value" && localConfig.defaultValue === oldValue) {
|
||||||
updateConfig("defaultValue", value);
|
newConfig.defaultValue = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLocalConfig(newConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 옵션 업데이트 완료 (onBlur)
|
||||||
|
const handleOptionBlur = () => {
|
||||||
|
onUpdateProperty("webTypeConfig", localConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 벌크 옵션 추가
|
// 벌크 옵션 추가
|
||||||
|
|
@ -185,8 +204,9 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="groupLabel"
|
id="groupLabel"
|
||||||
value={localConfig.groupLabel || ""}
|
value={localInputs.groupLabel}
|
||||||
onChange={(e) => updateConfig("groupLabel", e.target.value)}
|
onChange={(e) => setLocalInputs({ ...localInputs, groupLabel: e.target.value })}
|
||||||
|
onBlur={() => updateConfig("groupLabel", localInputs.groupLabel)}
|
||||||
placeholder="라디오버튼 그룹 제목"
|
placeholder="라디오버튼 그룹 제목"
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
|
|
@ -198,8 +218,9 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="groupName"
|
id="groupName"
|
||||||
value={localConfig.groupName || ""}
|
value={localInputs.groupName}
|
||||||
onChange={(e) => updateConfig("groupName", e.target.value)}
|
onChange={(e) => setLocalInputs({ ...localInputs, groupName: e.target.value })}
|
||||||
|
onBlur={() => updateConfig("groupName", localInputs.groupName)}
|
||||||
placeholder="자동 생성 (필드명 기반)"
|
placeholder="자동 생성 (필드명 기반)"
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
|
|
@ -290,22 +311,30 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
<Label className="text-xs">현재 옵션 ({localConfig.options.length}개)</Label>
|
<Label className="text-xs">현재 옵션 ({localConfig.options.length}개)</Label>
|
||||||
<div className="max-h-40 space-y-2 overflow-y-auto">
|
<div className="max-h-40 space-y-2 overflow-y-auto">
|
||||||
{localConfig.options.map((option, index) => (
|
{localConfig.options.map((option, index) => (
|
||||||
<div key={index} className="flex items-center gap-2 rounded border p-2">
|
<div key={`${option.value}-${index}`} className="flex items-center gap-2 rounded border p-2">
|
||||||
<Input
|
<Input
|
||||||
value={option.label}
|
value={option.label}
|
||||||
onChange={(e) => updateOption(index, "label", e.target.value)}
|
onChange={(e) => updateOptionLocal(index, "label", e.target.value)}
|
||||||
|
onBlur={handleOptionBlur}
|
||||||
placeholder="라벨"
|
placeholder="라벨"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={option.value}
|
value={option.value}
|
||||||
onChange={(e) => updateOption(index, "value", e.target.value)}
|
onChange={(e) => updateOptionLocal(index, "value", e.target.value)}
|
||||||
|
onBlur={handleOptionBlur}
|
||||||
placeholder="값"
|
placeholder="값"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
checked={!option.disabled}
|
checked={!option.disabled}
|
||||||
onCheckedChange={(checked) => updateOption(index, "disabled", !checked)}
|
onCheckedChange={(checked) => {
|
||||||
|
const newOptions = [...localConfig.options];
|
||||||
|
newOptions[index] = { ...newOptions[index], disabled: !checked };
|
||||||
|
const newConfig = { ...localConfig, options: newOptions };
|
||||||
|
setLocalConfig(newConfig);
|
||||||
|
onUpdateProperty("webTypeConfig", newConfig);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Button size="sm" variant="destructive" onClick={() => removeOption(index)} className="p-1 text-xs">
|
<Button size="sm" variant="destructive" onClick={() => removeOption(index)} className="p-1 text-xs">
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,12 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
const [newOptionLabel, setNewOptionLabel] = useState("");
|
const [newOptionLabel, setNewOptionLabel] = useState("");
|
||||||
const [newOptionValue, setNewOptionValue] = useState("");
|
const [newOptionValue, setNewOptionValue] = useState("");
|
||||||
const [bulkOptions, setBulkOptions] = useState("");
|
const [bulkOptions, setBulkOptions] = useState("");
|
||||||
|
|
||||||
|
// 입력 필드용 로컬 상태
|
||||||
|
const [localInputs, setLocalInputs] = useState({
|
||||||
|
placeholder: config.placeholder || "",
|
||||||
|
emptyMessage: config.emptyMessage || "",
|
||||||
|
});
|
||||||
|
|
||||||
// 컴포넌트 변경 시 로컬 상태 동기화
|
// 컴포넌트 변경 시 로컬 상태 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -61,6 +67,12 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
readonly: currentConfig.readonly || false,
|
readonly: currentConfig.readonly || false,
|
||||||
emptyMessage: currentConfig.emptyMessage || "선택 가능한 옵션이 없습니다",
|
emptyMessage: currentConfig.emptyMessage || "선택 가능한 옵션이 없습니다",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 입력 필드 로컬 상태도 동기화
|
||||||
|
setLocalInputs({
|
||||||
|
placeholder: currentConfig.placeholder || "",
|
||||||
|
emptyMessage: currentConfig.emptyMessage || "",
|
||||||
|
});
|
||||||
}, [widget.webTypeConfig]);
|
}, [widget.webTypeConfig]);
|
||||||
|
|
||||||
// 설정 업데이트 핸들러
|
// 설정 업데이트 핸들러
|
||||||
|
|
@ -91,11 +103,16 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
updateConfig("options", newOptions);
|
updateConfig("options", newOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 옵션 업데이트
|
// 옵션 업데이트 (입력 필드용 - 로컬 상태만)
|
||||||
const updateOption = (index: number, field: keyof SelectOption, value: any) => {
|
const updateOptionLocal = (index: number, field: keyof SelectOption, value: any) => {
|
||||||
const newOptions = [...localConfig.options];
|
const newOptions = [...localConfig.options];
|
||||||
newOptions[index] = { ...newOptions[index], [field]: value };
|
newOptions[index] = { ...newOptions[index], [field]: value };
|
||||||
updateConfig("options", newOptions);
|
setLocalConfig({ ...localConfig, options: newOptions });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 옵션 업데이트 완료 (onBlur)
|
||||||
|
const handleOptionBlur = () => {
|
||||||
|
onUpdateProperty("webTypeConfig", localConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 벌크 옵션 추가
|
// 벌크 옵션 추가
|
||||||
|
|
@ -170,8 +187,9 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="placeholder"
|
id="placeholder"
|
||||||
value={localConfig.placeholder || ""}
|
value={localInputs.placeholder}
|
||||||
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
onChange={(e) => setLocalInputs({ ...localInputs, placeholder: e.target.value })}
|
||||||
|
onBlur={() => updateConfig("placeholder", localInputs.placeholder)}
|
||||||
placeholder="선택하세요"
|
placeholder="선택하세요"
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
|
|
@ -183,8 +201,9 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="emptyMessage"
|
id="emptyMessage"
|
||||||
value={localConfig.emptyMessage || ""}
|
value={localInputs.emptyMessage}
|
||||||
onChange={(e) => updateConfig("emptyMessage", e.target.value)}
|
onChange={(e) => setLocalInputs({ ...localInputs, emptyMessage: e.target.value })}
|
||||||
|
onBlur={() => updateConfig("emptyMessage", localInputs.emptyMessage)}
|
||||||
placeholder="선택 가능한 옵션이 없습니다"
|
placeholder="선택 가능한 옵션이 없습니다"
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
|
|
@ -285,22 +304,30 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
<Label className="text-xs">현재 옵션 ({localConfig.options.length}개)</Label>
|
<Label className="text-xs">현재 옵션 ({localConfig.options.length}개)</Label>
|
||||||
<div className="max-h-40 space-y-2 overflow-y-auto">
|
<div className="max-h-40 space-y-2 overflow-y-auto">
|
||||||
{localConfig.options.map((option, index) => (
|
{localConfig.options.map((option, index) => (
|
||||||
<div key={index} className="flex items-center gap-2 rounded border p-2">
|
<div key={`${option.value}-${index}`} className="flex items-center gap-2 rounded border p-2">
|
||||||
<Input
|
<Input
|
||||||
value={option.label}
|
value={option.label}
|
||||||
onChange={(e) => updateOption(index, "label", e.target.value)}
|
onChange={(e) => updateOptionLocal(index, "label", e.target.value)}
|
||||||
|
onBlur={handleOptionBlur}
|
||||||
placeholder="라벨"
|
placeholder="라벨"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={option.value}
|
value={option.value}
|
||||||
onChange={(e) => updateOption(index, "value", e.target.value)}
|
onChange={(e) => updateOptionLocal(index, "value", e.target.value)}
|
||||||
|
onBlur={handleOptionBlur}
|
||||||
placeholder="값"
|
placeholder="값"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
checked={!option.disabled}
|
checked={!option.disabled}
|
||||||
onCheckedChange={(checked) => updateOption(index, "disabled", !checked)}
|
onCheckedChange={(checked) => {
|
||||||
|
const newOptions = [...localConfig.options];
|
||||||
|
newOptions[index] = { ...newOptions[index], disabled: !checked };
|
||||||
|
const newConfig = { ...localConfig, options: newOptions };
|
||||||
|
setLocalConfig(newConfig);
|
||||||
|
onUpdateProperty("webTypeConfig", newConfig);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Button size="sm" variant="destructive" onClick={() => removeOption(index)} className="p-1 text-xs">
|
<Button size="sm" variant="destructive" onClick={() => removeOption(index)} className="p-1 text-xs">
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
|
|
|
||||||
|
|
@ -78,3 +78,4 @@ export const numberingRuleTemplate = {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,10 +122,6 @@ const ResizableDialogContent = React.forwardRef<
|
||||||
|
|
||||||
// 1순위: userStyle에서 크기 추출 (화면관리에서 지정한 크기 - 항상 초기값으로 사용)
|
// 1순위: userStyle에서 크기 추출 (화면관리에서 지정한 크기 - 항상 초기값으로 사용)
|
||||||
if (userStyle) {
|
if (userStyle) {
|
||||||
console.log("🔍 userStyle 감지:", userStyle);
|
|
||||||
console.log("🔍 userStyle.width 타입:", typeof userStyle.width, "값:", userStyle.width);
|
|
||||||
console.log("🔍 userStyle.height 타입:", typeof userStyle.height, "값:", userStyle.height);
|
|
||||||
|
|
||||||
const styleWidth = typeof userStyle.width === 'string'
|
const styleWidth = typeof userStyle.width === 'string'
|
||||||
? parseInt(userStyle.width)
|
? parseInt(userStyle.width)
|
||||||
: userStyle.width;
|
: userStyle.width;
|
||||||
|
|
@ -133,31 +129,15 @@ const ResizableDialogContent = React.forwardRef<
|
||||||
? parseInt(userStyle.height)
|
? parseInt(userStyle.height)
|
||||||
: userStyle.height;
|
: userStyle.height;
|
||||||
|
|
||||||
console.log("📏 파싱된 크기:", {
|
|
||||||
styleWidth,
|
|
||||||
styleHeight,
|
|
||||||
"styleWidth truthy?": !!styleWidth,
|
|
||||||
"styleHeight truthy?": !!styleHeight,
|
|
||||||
minWidth,
|
|
||||||
maxWidth,
|
|
||||||
minHeight,
|
|
||||||
maxHeight
|
|
||||||
});
|
|
||||||
|
|
||||||
if (styleWidth && styleHeight) {
|
if (styleWidth && styleHeight) {
|
||||||
const finalSize = {
|
const finalSize = {
|
||||||
width: Math.max(minWidth, Math.min(maxWidth, styleWidth)),
|
width: Math.max(minWidth, Math.min(maxWidth, styleWidth)),
|
||||||
height: Math.max(minHeight, Math.min(maxHeight, styleHeight)),
|
height: Math.max(minHeight, Math.min(maxHeight, styleHeight)),
|
||||||
};
|
};
|
||||||
console.log("✅ userStyle 크기 사용:", finalSize);
|
|
||||||
return finalSize;
|
return finalSize;
|
||||||
} else {
|
|
||||||
console.log("❌ styleWidth 또는 styleHeight가 falsy:", { styleWidth, styleHeight });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("⚠️ userStyle 없음, defaultWidth/defaultHeight 사용:", { defaultWidth, defaultHeight });
|
|
||||||
|
|
||||||
// 2순위: 현재 렌더링된 크기 사용 (주석처리 - 모달이 열린 후 늘어나는 현상 방지)
|
// 2순위: 현재 렌더링된 크기 사용 (주석처리 - 모달이 열린 후 늘어나는 현상 방지)
|
||||||
// if (contentRef.current) {
|
// if (contentRef.current) {
|
||||||
// const rect = contentRef.current.getBoundingClientRect();
|
// const rect = contentRef.current.getBoundingClientRect();
|
||||||
|
|
@ -209,7 +189,6 @@ const ResizableDialogContent = React.forwardRef<
|
||||||
// 사용자가 리사이징한 크기 우선
|
// 사용자가 리사이징한 크기 우선
|
||||||
setSize({ width: savedSize.width, height: savedSize.height });
|
setSize({ width: savedSize.width, height: savedSize.height });
|
||||||
setUserResized(true);
|
setUserResized(true);
|
||||||
console.log("✅ 사용자 리사이징 크기 적용:", savedSize);
|
|
||||||
} else if (userStyle && userStyle.width && userStyle.height) {
|
} else if (userStyle && userStyle.width && userStyle.height) {
|
||||||
// 화면관리에서 설정한 크기
|
// 화면관리에서 설정한 크기
|
||||||
const styleWidth = typeof userStyle.width === 'string'
|
const styleWidth = typeof userStyle.width === 'string'
|
||||||
|
|
@ -224,7 +203,6 @@ const ResizableDialogContent = React.forwardRef<
|
||||||
width: Math.max(minWidth, Math.min(maxWidth, styleWidth)),
|
width: Math.max(minWidth, Math.min(maxWidth, styleWidth)),
|
||||||
height: Math.max(minHeight, Math.min(maxHeight, styleHeight)),
|
height: Math.max(minHeight, Math.min(maxHeight, styleHeight)),
|
||||||
};
|
};
|
||||||
console.log("🔄 userStyle 크기 적용:", newSize);
|
|
||||||
setSize(newSize);
|
setSize(newSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useMemo } from "react";
|
import React, { useState, useMemo, useEffect } from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -34,6 +34,21 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [localFields, setLocalFields] = useState<RepeaterFieldDefinition[]>(config.fields || []);
|
const [localFields, setLocalFields] = useState<RepeaterFieldDefinition[]>(config.fields || []);
|
||||||
const [fieldNamePopoverOpen, setFieldNamePopoverOpen] = useState<Record<number, boolean>>({});
|
const [fieldNamePopoverOpen, setFieldNamePopoverOpen] = useState<Record<number, boolean>>({});
|
||||||
|
|
||||||
|
// 로컬 입력 상태 (각 필드의 라벨, placeholder 등)
|
||||||
|
const [localInputs, setLocalInputs] = useState<Record<number, { label: string; placeholder: string }>>({});
|
||||||
|
|
||||||
|
// 설정 입력 필드의 로컬 상태
|
||||||
|
const [localConfigInputs, setLocalConfigInputs] = useState({
|
||||||
|
addButtonText: config.addButtonText || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// config 변경 시 로컬 상태 동기화
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalConfigInputs({
|
||||||
|
addButtonText: config.addButtonText || "",
|
||||||
|
});
|
||||||
|
}, [config.addButtonText]);
|
||||||
|
|
||||||
// 이미 사용된 컬럼명 목록
|
// 이미 사용된 컬럼명 목록
|
||||||
const usedColumnNames = useMemo(() => {
|
const usedColumnNames = useMemo(() => {
|
||||||
|
|
@ -72,7 +87,32 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
|
||||||
handleFieldsChange(localFields.filter((_, i) => i !== index));
|
handleFieldsChange(localFields.filter((_, i) => i !== index));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 필드 수정
|
// 필드 수정 (입력 중 - 로컬 상태만)
|
||||||
|
const updateFieldLocal = (index: number, field: 'label' | 'placeholder', value: string) => {
|
||||||
|
setLocalInputs(prev => ({
|
||||||
|
...prev,
|
||||||
|
[index]: {
|
||||||
|
...prev[index],
|
||||||
|
[field]: value
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 필드 수정 완료 (onBlur - 실제 업데이트)
|
||||||
|
const handleFieldBlur = (index: number) => {
|
||||||
|
const localInput = localInputs[index];
|
||||||
|
if (localInput) {
|
||||||
|
const newFields = [...localFields];
|
||||||
|
newFields[index] = {
|
||||||
|
...newFields[index],
|
||||||
|
label: localInput.label,
|
||||||
|
placeholder: localInput.placeholder
|
||||||
|
};
|
||||||
|
handleFieldsChange(newFields);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 필드 수정 (즉시 반영 - 드롭다운, 체크박스 등)
|
||||||
const updateField = (index: number, updates: Partial<RepeaterFieldDefinition>) => {
|
const updateField = (index: number, updates: Partial<RepeaterFieldDefinition>) => {
|
||||||
const newFields = [...localFields];
|
const newFields = [...localFields];
|
||||||
newFields[index] = { ...newFields[index], ...updates };
|
newFields[index] = { ...newFields[index], ...updates };
|
||||||
|
|
@ -157,7 +197,7 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
|
||||||
<Label className="text-sm font-semibold">필드 정의</Label>
|
<Label className="text-sm font-semibold">필드 정의</Label>
|
||||||
|
|
||||||
{localFields.map((field, index) => (
|
{localFields.map((field, index) => (
|
||||||
<Card key={index} className="border-2">
|
<Card key={`${field.name}-${index}`} className="border-2">
|
||||||
<CardContent className="space-y-3 pt-4">
|
<CardContent className="space-y-3 pt-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm font-semibold text-gray-700">필드 {index + 1}</span>
|
<span className="text-sm font-semibold text-gray-700">필드 {index + 1}</span>
|
||||||
|
|
@ -200,6 +240,14 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
|
||||||
label: column.columnLabel || column.columnName,
|
label: column.columnLabel || column.columnName,
|
||||||
type: (column.widgetType as RepeaterFieldType) || "text",
|
type: (column.widgetType as RepeaterFieldType) || "text",
|
||||||
});
|
});
|
||||||
|
// 로컬 입력 상태도 업데이트
|
||||||
|
setLocalInputs(prev => ({
|
||||||
|
...prev,
|
||||||
|
[index]: {
|
||||||
|
label: column.columnLabel || column.columnName,
|
||||||
|
placeholder: prev[index]?.placeholder || ""
|
||||||
|
}
|
||||||
|
}));
|
||||||
setFieldNamePopoverOpen({ ...fieldNamePopoverOpen, [index]: false });
|
setFieldNamePopoverOpen({ ...fieldNamePopoverOpen, [index]: false });
|
||||||
}}
|
}}
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
|
|
@ -225,8 +273,9 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">라벨</Label>
|
<Label className="text-xs">라벨</Label>
|
||||||
<Input
|
<Input
|
||||||
value={field.label}
|
value={localInputs[index]?.label !== undefined ? localInputs[index].label : field.label}
|
||||||
onChange={(e) => updateField(index, { label: e.target.value })}
|
onChange={(e) => updateFieldLocal(index, 'label', e.target.value)}
|
||||||
|
onBlur={() => handleFieldBlur(index)}
|
||||||
placeholder="필드 라벨"
|
placeholder="필드 라벨"
|
||||||
className="h-8 w-full text-xs"
|
className="h-8 w-full text-xs"
|
||||||
/>
|
/>
|
||||||
|
|
@ -258,10 +307,11 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">Placeholder</Label>
|
<Label className="text-xs">Placeholder</Label>
|
||||||
<Input
|
<Input
|
||||||
value={field.placeholder || ""}
|
value={localInputs[index]?.placeholder !== undefined ? localInputs[index].placeholder : (field.placeholder || "")}
|
||||||
onChange={(e) => updateField(index, { placeholder: e.target.value })}
|
onChange={(e) => updateFieldLocal(index, 'placeholder', e.target.value)}
|
||||||
|
onBlur={() => handleFieldBlur(index)}
|
||||||
placeholder="입력 안내"
|
placeholder="입력 안내"
|
||||||
className="h-8 w-full"
|
className="h-8 w-full text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -329,8 +379,9 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
|
||||||
<Input
|
<Input
|
||||||
id="repeater-add-button-text"
|
id="repeater-add-button-text"
|
||||||
type="text"
|
type="text"
|
||||||
value={config.addButtonText || ""}
|
value={localConfigInputs.addButtonText}
|
||||||
onChange={(e) => handleChange("addButtonText", e.target.value)}
|
onChange={(e) => setLocalConfigInputs({ ...localConfigInputs, addButtonText: e.target.value })}
|
||||||
|
onBlur={() => handleChange("addButtonText", localConfigInputs.addButtonText)}
|
||||||
placeholder="항목 추가"
|
placeholder="항목 추가"
|
||||||
className="h-8"
|
className="h-8"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,25 @@ export async function getCategoryColumns(tableName: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴별 카테고리 컬럼 목록 조회
|
||||||
|
*
|
||||||
|
* @param menuObjid 메뉴 OBJID
|
||||||
|
* @returns 해당 메뉴와 상위 메뉴들이 설정한 모든 카테고리 컬럼
|
||||||
|
*/
|
||||||
|
export async function getCategoryColumnsByMenu(menuObjid: number) {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<{
|
||||||
|
success: boolean;
|
||||||
|
data: CategoryColumn[];
|
||||||
|
}>(`/table-management/menu/${menuObjid}/category-columns`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("메뉴별 카테고리 컬럼 조회 실패:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 카테고리 값 목록 조회 (메뉴 스코프)
|
* 카테고리 값 목록 조회 (메뉴 스코프)
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -289,17 +289,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
// modal-repeater-table은 배열 데이터를 다루므로 빈 배열로 초기화
|
// modal-repeater-table은 배열 데이터를 다루므로 빈 배열로 초기화
|
||||||
let currentValue;
|
let currentValue;
|
||||||
if (componentType === "modal-repeater-table") {
|
if (componentType === "modal-repeater-table") {
|
||||||
// 🆕 EditModal에서 전달된 groupedData가 있으면 우선 사용
|
// EditModal에서 전달된 groupedData가 있으면 우선 사용
|
||||||
currentValue = props.groupedData || formData?.[fieldName] || [];
|
currentValue = props.groupedData || formData?.[fieldName] || [];
|
||||||
|
|
||||||
// 디버깅 로그
|
|
||||||
console.log("🔍 [DynamicComponentRenderer] ModalRepeaterTable value 설정:", {
|
|
||||||
hasGroupedData: !!props.groupedData,
|
|
||||||
groupedDataLength: props.groupedData?.length || 0,
|
|
||||||
fieldName,
|
|
||||||
formDataValue: formData?.[fieldName],
|
|
||||||
finalValueLength: Array.isArray(currentValue) ? currentValue.length : 0,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
currentValue = formData?.[fieldName] || "";
|
currentValue = formData?.[fieldName] || "";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,6 @@ import { ConditionalContainerProps, ConditionalSection } from "./types";
|
||||||
import { ConditionalSectionViewer } from "./ConditionalSectionViewer";
|
import { ConditionalSectionViewer } from "./ConditionalSectionViewer";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
console.log("🚀 ConditionalContainerComponent 모듈 로드됨!");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 조건부 컨테이너 컴포넌트
|
* 조건부 컨테이너 컴포넌트
|
||||||
* 상단 셀렉트박스 값에 따라 하단에 다른 UI를 표시
|
* 상단 셀렉트박스 값에 따라 하단에 다른 UI를 표시
|
||||||
|
|
@ -43,11 +41,6 @@ export function ConditionalContainerComponent({
|
||||||
groupedData, // 🆕 그룹 데이터
|
groupedData, // 🆕 그룹 데이터
|
||||||
onSave, // 🆕 EditModal의 handleSave 콜백
|
onSave, // 🆕 EditModal의 handleSave 콜백
|
||||||
}: ConditionalContainerProps) {
|
}: ConditionalContainerProps) {
|
||||||
console.log("🎯 ConditionalContainerComponent 렌더링!", {
|
|
||||||
isDesignMode,
|
|
||||||
hasOnHeightChange: !!onHeightChange,
|
|
||||||
componentId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// config prop 우선, 없으면 개별 prop 사용
|
// config prop 우선, 없으면 개별 prop 사용
|
||||||
const controlField = config?.controlField || propControlField || "condition";
|
const controlField = config?.controlField || propControlField || "condition";
|
||||||
|
|
@ -86,24 +79,8 @@ export function ConditionalContainerComponent({
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const previousHeightRef = useRef<number>(0);
|
const previousHeightRef = useRef<number>(0);
|
||||||
|
|
||||||
// 🔍 디버그: props 확인
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("🔍 ConditionalContainer props:", {
|
|
||||||
isDesignMode,
|
|
||||||
hasOnHeightChange: !!onHeightChange,
|
|
||||||
componentId,
|
|
||||||
selectedValue,
|
|
||||||
});
|
|
||||||
}, [isDesignMode, onHeightChange, componentId, selectedValue]);
|
|
||||||
|
|
||||||
// 높이 변화 감지 및 콜백 호출
|
// 높이 변화 감지 및 콜백 호출
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("🔍 ResizeObserver 등록 조건:", {
|
|
||||||
hasContainer: !!containerRef.current,
|
|
||||||
isDesignMode,
|
|
||||||
hasOnHeightChange: !!onHeightChange,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!containerRef.current || isDesignMode || !onHeightChange) return;
|
if (!containerRef.current || isDesignMode || !onHeightChange) return;
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver((entries) => {
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
|
|
||||||
|
|
@ -195,17 +195,57 @@ export function ModalRepeaterTableComponent({
|
||||||
const columnName = component?.columnName;
|
const columnName = component?.columnName;
|
||||||
const value = (columnName && formData?.[columnName]) || componentConfig?.value || propValue || [];
|
const value = (columnName && formData?.[columnName]) || componentConfig?.value || propValue || [];
|
||||||
|
|
||||||
// ✅ onChange 래퍼 (기존 onChange 콜백 + onFormDataChange 호출)
|
// ✅ onChange 래퍼 (기존 onChange 콜백 + onFormDataChange 호출 + 납기일 일괄 적용)
|
||||||
const handleChange = (newData: any[]) => {
|
const handleChange = (newData: any[]) => {
|
||||||
|
console.log("🔄 ModalRepeaterTableComponent.handleChange 호출:", {
|
||||||
|
dataLength: newData.length,
|
||||||
|
columnName,
|
||||||
|
hasExternalOnChange: !!(componentConfig?.onChange || propOnChange),
|
||||||
|
hasOnFormDataChange: !!(onFormDataChange && columnName),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🆕 납기일 일괄 적용 로직 (납기일 필드가 있는 경우만)
|
||||||
|
let processedData = newData;
|
||||||
|
|
||||||
|
// 납기일 필드 찾기 (item_due_date, delivery_date, due_date 등)
|
||||||
|
const dateField = columns.find(
|
||||||
|
(col) =>
|
||||||
|
col.field === "item_due_date" ||
|
||||||
|
col.field === "delivery_date" ||
|
||||||
|
col.field === "due_date"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dateField && !isDeliveryDateApplied && newData.length > 0) {
|
||||||
|
// 현재 상태: 납기일이 있는 행과 없는 행 개수 체크
|
||||||
|
const itemsWithDate = newData.filter((item) => item[dateField.field]);
|
||||||
|
const itemsWithoutDate = newData.filter((item) => !item[dateField.field]);
|
||||||
|
|
||||||
|
// 정확히 1개만 날짜가 있고, 나머지는 모두 비어있을 때 일괄 적용
|
||||||
|
if (itemsWithDate.length === 1 && itemsWithoutDate.length > 0) {
|
||||||
|
const selectedDate = itemsWithDate[0][dateField.field];
|
||||||
|
processedData = newData.map((item) => ({
|
||||||
|
...item,
|
||||||
|
[dateField.field]: selectedDate, // 모든 행에 동일한 납기일 적용
|
||||||
|
}));
|
||||||
|
|
||||||
|
setIsDeliveryDateApplied(true); // 플래그 활성화
|
||||||
|
|
||||||
|
console.log("✅ 납기일 일괄 적용 완료:", selectedDate);
|
||||||
|
console.log(` - 대상: ${itemsWithoutDate.length}개 행에 ${selectedDate} 적용`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 기존 onChange 콜백 호출 (호환성)
|
// 기존 onChange 콜백 호출 (호환성)
|
||||||
const externalOnChange = componentConfig?.onChange || propOnChange;
|
const externalOnChange = componentConfig?.onChange || propOnChange;
|
||||||
if (externalOnChange) {
|
if (externalOnChange) {
|
||||||
externalOnChange(newData);
|
console.log("📤 외부 onChange 호출");
|
||||||
|
externalOnChange(processedData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 onFormDataChange 호출하여 EditModal의 groupData 업데이트
|
// 🆕 onFormDataChange 호출하여 EditModal의 groupData 업데이트
|
||||||
if (onFormDataChange && columnName) {
|
if (onFormDataChange && columnName) {
|
||||||
onFormDataChange(columnName, newData);
|
console.log("📤 onFormDataChange 호출:", columnName);
|
||||||
|
onFormDataChange(columnName, processedData);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -219,18 +259,19 @@ export function ModalRepeaterTableComponent({
|
||||||
const companyCode = componentConfig?.companyCode || propCompanyCode;
|
const companyCode = componentConfig?.companyCode || propCompanyCode;
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
|
||||||
|
// 🆕 납기일 일괄 적용 플래그 (딱 한 번만 실행)
|
||||||
|
const [isDeliveryDateApplied, setIsDeliveryDateApplied] = useState(false);
|
||||||
|
|
||||||
// columns가 비어있으면 sourceColumns로부터 자동 생성
|
// columns가 비어있으면 sourceColumns로부터 자동 생성
|
||||||
const columns = React.useMemo((): RepeaterColumnConfig[] => {
|
const columns = React.useMemo((): RepeaterColumnConfig[] => {
|
||||||
const configuredColumns = componentConfig?.columns || propColumns || [];
|
const configuredColumns = componentConfig?.columns || propColumns || [];
|
||||||
|
|
||||||
if (configuredColumns.length > 0) {
|
if (configuredColumns.length > 0) {
|
||||||
console.log("✅ 설정된 columns 사용:", configuredColumns);
|
|
||||||
return configuredColumns;
|
return configuredColumns;
|
||||||
}
|
}
|
||||||
|
|
||||||
// columns가 비어있으면 sourceColumns로부터 자동 생성
|
// columns가 비어있으면 sourceColumns로부터 자동 생성
|
||||||
if (sourceColumns.length > 0) {
|
if (sourceColumns.length > 0) {
|
||||||
console.log("🔄 sourceColumns로부터 자동 생성:", sourceColumns);
|
|
||||||
const autoColumns: RepeaterColumnConfig[] = sourceColumns.map((field) => ({
|
const autoColumns: RepeaterColumnConfig[] = sourceColumns.map((field) => ({
|
||||||
field: field,
|
field: field,
|
||||||
label: field, // 필드명을 라벨로 사용 (나중에 설정에서 변경 가능)
|
label: field, // 필드명을 라벨로 사용 (나중에 설정에서 변경 가능)
|
||||||
|
|
@ -238,99 +279,72 @@ export function ModalRepeaterTableComponent({
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
width: "150px",
|
width: "150px",
|
||||||
}));
|
}));
|
||||||
console.log("📋 자동 생성된 columns:", autoColumns);
|
|
||||||
return autoColumns;
|
return autoColumns;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn("⚠️ columns와 sourceColumns 모두 비어있음!");
|
console.warn("⚠️ [ModalRepeaterTable] columns와 sourceColumns 모두 비어있음!");
|
||||||
return [];
|
return [];
|
||||||
}, [componentConfig?.columns, propColumns, sourceColumns]);
|
}, [componentConfig?.columns, propColumns, sourceColumns]);
|
||||||
|
|
||||||
// 초기 props 로깅
|
// 초기 props 검증
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rawSourceColumns.length !== sourceColumns.length) {
|
if (rawSourceColumns.length !== sourceColumns.length) {
|
||||||
console.warn(`⚠️ sourceColumns 필터링: ${rawSourceColumns.length}개 → ${sourceColumns.length}개 (빈 문자열 제거)`);
|
console.warn(`⚠️ [ModalRepeaterTable] sourceColumns 필터링: ${rawSourceColumns.length}개 → ${sourceColumns.length}개`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rawUniqueField !== uniqueField) {
|
if (rawUniqueField !== uniqueField) {
|
||||||
console.warn(`⚠️ uniqueField 자동 보정: "${rawUniqueField}" → "${uniqueField}"`);
|
console.warn(`⚠️ [ModalRepeaterTable] uniqueField 자동 보정: "${rawUniqueField}" → "${uniqueField}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("🎬 ModalRepeaterTableComponent 마운트:", {
|
|
||||||
columnsLength: columns.length,
|
|
||||||
sourceTable,
|
|
||||||
sourceColumns,
|
|
||||||
uniqueField,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (columns.length === 0) {
|
if (columns.length === 0) {
|
||||||
console.error("❌ columns가 비어있습니다! sourceColumns:", sourceColumns);
|
console.error("❌ [ModalRepeaterTable] columns가 비어있습니다!", { sourceColumns });
|
||||||
} else {
|
|
||||||
console.log("✅ columns 설정 완료:", columns.map(c => c.label || c.field).join(", "));
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// value 변경 감지
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("📦 ModalRepeaterTableComponent value 변경:", {
|
|
||||||
valueLength: value.length,
|
|
||||||
});
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
// 🆕 저장 요청 시에만 데이터 전달 (beforeFormSave 이벤트 리스너)
|
// 🆕 저장 요청 시에만 데이터 전달 (beforeFormSave 이벤트 리스너)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleSaveRequest = async (event: Event) => {
|
const handleSaveRequest = async (event: Event) => {
|
||||||
const componentKey = columnName || component?.id || "modal_repeater_data";
|
const componentKey = columnName || component?.id || "modal_repeater_data";
|
||||||
|
|
||||||
console.log("🔔 [ModalRepeaterTable] beforeFormSave 이벤트 수신!", {
|
|
||||||
componentKey,
|
|
||||||
itemsCount: value.length,
|
|
||||||
hasOnFormDataChange: !!onFormDataChange,
|
|
||||||
columnName,
|
|
||||||
componentId: component?.id,
|
|
||||||
targetTable,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (value.length === 0) {
|
if (value.length === 0) {
|
||||||
console.warn("⚠️ [ModalRepeaterTable] 저장할 데이터 없음");
|
console.warn("⚠️ [ModalRepeaterTable] 저장할 데이터 없음");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 sourceColumns에 포함된 컬럼 제외 (조인된 컬럼 제거)
|
// sourceColumns에 포함된 컬럼 제외 (조인된 컬럼 제거)
|
||||||
console.log("🔍 [ModalRepeaterTable] 필터링 전 데이터:", {
|
// 단, columnMappings에 정의된 컬럼은 저장해야 하므로 제외하지 않음
|
||||||
sourceColumns,
|
const mappedFields = columns
|
||||||
sourceTable,
|
.filter(col => col.mapping?.type === "source" && col.mapping?.sourceField)
|
||||||
targetTable,
|
.map(col => col.field);
|
||||||
sampleItem: value[0],
|
|
||||||
itemKeys: value[0] ? Object.keys(value[0]) : [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredData = value.map((item: any) => {
|
const filteredData = value.map((item: any) => {
|
||||||
const filtered: Record<string, any> = {};
|
const filtered: Record<string, any> = {};
|
||||||
|
|
||||||
Object.keys(item).forEach((key) => {
|
Object.keys(item).forEach((key) => {
|
||||||
// sourceColumns에 포함된 컬럼은 제외 (item_info 테이블의 컬럼)
|
// 메타데이터 필드 제외
|
||||||
if (sourceColumns.includes(key)) {
|
|
||||||
console.log(` ⛔ ${key} 제외 (sourceColumn)`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 메타데이터 필드도 제외
|
|
||||||
if (key.startsWith("_")) {
|
if (key.startsWith("_")) {
|
||||||
console.log(` ⛔ ${key} 제외 (메타데이터)`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sourceColumns에 포함되어 있지만 columnMappings에도 정의된 경우 → 저장함
|
||||||
|
if (mappedFields.includes(key)) {
|
||||||
|
filtered[key] = item[key];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sourceColumns에만 있고 매핑 안 된 경우 → 제외 (조인 전용)
|
||||||
|
if (sourceColumns.includes(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 나머지는 모두 저장
|
||||||
filtered[key] = item[key];
|
filtered[key] = item[key];
|
||||||
});
|
});
|
||||||
|
|
||||||
return filtered;
|
return filtered;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("✅ [ModalRepeaterTable] 필터링 후 데이터:", {
|
// targetTable 메타데이터를 배열 항목에 추가
|
||||||
filteredItemKeys: filteredData[0] ? Object.keys(filteredData[0]) : [],
|
|
||||||
sampleFilteredItem: filteredData[0],
|
|
||||||
});
|
|
||||||
|
|
||||||
// 🔥 targetTable 메타데이터를 배열 항목에 추가
|
|
||||||
const dataWithTargetTable = targetTable
|
const dataWithTargetTable = targetTable
|
||||||
? filteredData.map((item: any) => ({
|
? filteredData.map((item: any) => ({
|
||||||
...item,
|
...item,
|
||||||
|
|
@ -338,21 +352,19 @@ export function ModalRepeaterTableComponent({
|
||||||
}))
|
}))
|
||||||
: filteredData;
|
: filteredData;
|
||||||
|
|
||||||
// ✅ CustomEvent의 detail에 데이터 추가
|
// CustomEvent의 detail에 데이터 추가
|
||||||
if (event instanceof CustomEvent && event.detail) {
|
if (event instanceof CustomEvent && event.detail) {
|
||||||
event.detail.formData[componentKey] = dataWithTargetTable;
|
event.detail.formData[componentKey] = dataWithTargetTable;
|
||||||
console.log("✅ [ModalRepeaterTable] context.formData에 데이터 추가 완료:", {
|
console.log("✅ [ModalRepeaterTable] 저장 데이터 준비:", {
|
||||||
key: componentKey,
|
key: componentKey,
|
||||||
itemCount: dataWithTargetTable.length,
|
itemCount: dataWithTargetTable.length,
|
||||||
targetTable: targetTable || "미설정 (화면 설계에서 설정 필요)",
|
targetTable: targetTable || "미설정",
|
||||||
sampleItem: dataWithTargetTable[0],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 기존 onFormDataChange도 호출 (호환성)
|
// 기존 onFormDataChange도 호출 (호환성)
|
||||||
if (onFormDataChange) {
|
if (onFormDataChange) {
|
||||||
onFormDataChange(componentKey, dataWithTargetTable);
|
onFormDataChange(componentKey, dataWithTargetTable);
|
||||||
console.log("✅ [ModalRepeaterTable] onFormDataChange 호출 완료");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,14 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { Check, ChevronsUpDown } from "lucide-react";
|
import { Check, ChevronsUpDown } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { getSecondLevelMenus, getCategoryColumns, getCategoryValues } from "@/lib/api/tableCategoryValue";
|
import { getSecondLevelMenus, getCategoryColumns, getCategoryColumnsByMenu, getCategoryValues } from "@/lib/api/tableCategoryValue";
|
||||||
import { CalculationBuilder } from "./CalculationBuilder";
|
import { CalculationBuilder } from "./CalculationBuilder";
|
||||||
|
|
||||||
export interface SelectedItemsDetailInputConfigPanelProps {
|
export interface SelectedItemsDetailInputConfigPanelProps {
|
||||||
config: SelectedItemsDetailInputConfig;
|
config: SelectedItemsDetailInputConfig;
|
||||||
onChange: (config: Partial<SelectedItemsDetailInputConfig>) => void;
|
onChange: (config: Partial<SelectedItemsDetailInputConfig>) => void;
|
||||||
sourceTableColumns?: Array<{ columnName: string; columnLabel?: string; dataType?: string }>; // 🆕 원본 테이블 컬럼
|
sourceTableColumns?: Array<{ columnName: string; columnLabel?: string; dataType?: string; inputType?: string }>; // 🆕 원본 테이블 컬럼 (inputType 추가)
|
||||||
targetTableColumns?: Array<{ columnName: string; columnLabel?: string; dataType?: string }>; // 🆕 대상 테이블 컬럼
|
targetTableColumns?: Array<{ columnName: string; columnLabel?: string; dataType?: string; inputType?: string; codeCategory?: string }>; // 🆕 대상 테이블 컬럼 (inputType, codeCategory 추가)
|
||||||
allTables?: Array<{ tableName: string; displayName?: string }>;
|
allTables?: Array<{ tableName: string; displayName?: string }>;
|
||||||
screenTableName?: string; // 🆕 현재 화면의 테이블명 (자동 설정용)
|
screenTableName?: string; // 🆕 현재 화면의 테이블명 (자동 설정용)
|
||||||
onSourceTableChange?: (tableName: string) => void; // 🆕 원본 테이블 변경 콜백
|
onSourceTableChange?: (tableName: string) => void; // 🆕 원본 테이블 변경 콜백
|
||||||
|
|
@ -53,9 +53,15 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
// 🆕 그룹 입력값을 위한 로컬 상태 (포커스 유지용)
|
// 🆕 그룹 입력값을 위한 로컬 상태 (포커스 유지용)
|
||||||
const [localGroupInputs, setLocalGroupInputs] = useState<Record<string, { id?: string; title?: string; description?: string; order?: number }>>({});
|
const [localGroupInputs, setLocalGroupInputs] = useState<Record<string, { id?: string; title?: string; description?: string; order?: number }>>({});
|
||||||
|
|
||||||
|
// 🆕 필드 입력값을 위한 로컬 상태 (포커스 유지용)
|
||||||
|
const [localFieldInputs, setLocalFieldInputs] = useState<Record<number, { label?: string; placeholder?: string }>>({});
|
||||||
|
|
||||||
// 🆕 표시 항목의 입력값을 위한 로컬 상태 (포커스 유지용)
|
// 🆕 표시 항목의 입력값을 위한 로컬 상태 (포커스 유지용)
|
||||||
const [localDisplayItemInputs, setLocalDisplayItemInputs] = useState<Record<string, Record<number, { label?: string; value?: string }>>>({});
|
const [localDisplayItemInputs, setLocalDisplayItemInputs] = useState<Record<string, Record<number, { label?: string; value?: string }>>>({});
|
||||||
|
|
||||||
|
// 🆕 부모 데이터 매핑의 기본값 입력을 위한 로컬 상태 (포커스 유지용)
|
||||||
|
const [localMappingInputs, setLocalMappingInputs] = useState<Record<number, string>>({});
|
||||||
|
|
||||||
|
|
||||||
// 🆕 그룹별 펼침/접힘 상태
|
// 🆕 그룹별 펼침/접힘 상태
|
||||||
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
||||||
|
|
@ -63,6 +69,13 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
// 🆕 그룹별 표시 항목 설정 펼침/접힘 상태
|
// 🆕 그룹별 표시 항목 설정 펼침/접힘 상태
|
||||||
const [expandedDisplayItems, setExpandedDisplayItems] = useState<Record<string, boolean>>({});
|
const [expandedDisplayItems, setExpandedDisplayItems] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
// 🆕 카테고리 매핑 아코디언 펼침/접힘 상태
|
||||||
|
const [expandedCategoryMappings, setExpandedCategoryMappings] = useState<Record<string, boolean>>({
|
||||||
|
discountType: false,
|
||||||
|
roundingType: false,
|
||||||
|
roundingUnit: false,
|
||||||
|
});
|
||||||
|
|
||||||
// 🆕 원본 테이블 선택 상태
|
// 🆕 원본 테이블 선택 상태
|
||||||
const [sourceTableSelectOpen, setSourceTableSelectOpen] = useState(false);
|
const [sourceTableSelectOpen, setSourceTableSelectOpen] = useState(false);
|
||||||
const [sourceTableSearchValue, setSourceTableSearchValue] = useState("");
|
const [sourceTableSearchValue, setSourceTableSearchValue] = useState("");
|
||||||
|
|
@ -83,8 +96,8 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
const [autoFillTableColumns, setAutoFillTableColumns] = useState<Record<number, Array<{ columnName: string; columnLabel?: string; dataType?: string }>>>({});
|
const [autoFillTableColumns, setAutoFillTableColumns] = useState<Record<number, Array<{ columnName: string; columnLabel?: string; dataType?: string }>>>({});
|
||||||
|
|
||||||
// 🆕 원본/대상 테이블 컬럼 상태 (내부에서 로드)
|
// 🆕 원본/대상 테이블 컬럼 상태 (내부에서 로드)
|
||||||
const [loadedSourceTableColumns, setLoadedSourceTableColumns] = useState<Array<{ columnName: string; columnLabel?: string; dataType?: string }>>([]);
|
const [loadedSourceTableColumns, setLoadedSourceTableColumns] = useState<Array<{ columnName: string; columnLabel?: string; dataType?: string; inputType?: string }>>([]);
|
||||||
const [loadedTargetTableColumns, setLoadedTargetTableColumns] = useState<Array<{ columnName: string; columnLabel?: string; dataType?: string }>>([]);
|
const [loadedTargetTableColumns, setLoadedTargetTableColumns] = useState<Array<{ columnName: string; columnLabel?: string; dataType?: string; inputType?: string; codeCategory?: string }>>([]);
|
||||||
|
|
||||||
// 🆕 원본 테이블 컬럼 로드
|
// 🆕 원본 테이블 컬럼 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -105,6 +118,7 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
columnName: col.columnName,
|
columnName: col.columnName,
|
||||||
columnLabel: col.displayName || col.columnLabel || col.columnName,
|
columnLabel: col.displayName || col.columnLabel || col.columnName,
|
||||||
dataType: col.dataType,
|
dataType: col.dataType,
|
||||||
|
inputType: col.inputType, // 🔧 inputType 추가
|
||||||
})));
|
})));
|
||||||
console.log("✅ 원본 테이블 컬럼 로드 성공:", columns.length);
|
console.log("✅ 원본 테이블 컬럼 로드 성공:", columns.length);
|
||||||
}
|
}
|
||||||
|
|
@ -135,6 +149,7 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
columnName: col.columnName,
|
columnName: col.columnName,
|
||||||
columnLabel: col.displayName || col.columnLabel || col.columnName,
|
columnLabel: col.displayName || col.columnLabel || col.columnName,
|
||||||
dataType: col.dataType,
|
dataType: col.dataType,
|
||||||
|
inputType: col.inputType, // 🔧 inputType 추가
|
||||||
})));
|
})));
|
||||||
console.log("✅ 대상 테이블 컬럼 로드 성공:", columns.length);
|
console.log("✅ 대상 테이블 컬럼 로드 성공:", columns.length);
|
||||||
}
|
}
|
||||||
|
|
@ -165,6 +180,18 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
});
|
});
|
||||||
return newInputs;
|
return newInputs;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 🔧 표시 항목이 있는 그룹은 아코디언을 열린 상태로 초기화
|
||||||
|
setExpandedDisplayItems(prev => {
|
||||||
|
const newExpanded = { ...prev };
|
||||||
|
(config.fieldGroups || []).forEach(group => {
|
||||||
|
// 이미 상태가 있으면 유지, 없으면 displayItems가 있을 때만 열기
|
||||||
|
if (!(group.id in newExpanded) && group.displayItems && group.displayItems.length > 0) {
|
||||||
|
newExpanded[group.id] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return newExpanded;
|
||||||
|
});
|
||||||
}, [config.fieldGroups]);
|
}, [config.fieldGroups]);
|
||||||
|
|
||||||
// 🆕 초기 렌더링 시 기존 필드들의 autoFillFromTable 컬럼 로드
|
// 🆕 초기 렌더링 시 기존 필드들의 autoFillFromTable 컬럼 로드
|
||||||
|
|
@ -238,6 +265,36 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 🆕 저장된 부모 데이터 매핑의 컬럼 자동 로드
|
||||||
|
useEffect(() => {
|
||||||
|
const loadSavedMappingColumns = async () => {
|
||||||
|
if (!config.parentDataMapping || config.parentDataMapping.length === 0) {
|
||||||
|
console.log("📭 [부모 데이터 매핑] 매핑이 없습니다");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🔍 [부모 데이터 매핑] 저장된 매핑 컬럼 자동 로드 시작:", config.parentDataMapping.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < config.parentDataMapping.length; i++) {
|
||||||
|
const mapping = config.parentDataMapping[i];
|
||||||
|
|
||||||
|
// 이미 로드된 컬럼이 있으면 스킵
|
||||||
|
if (mappingSourceColumns[i] && mappingSourceColumns[i].length > 0) {
|
||||||
|
console.log(`⏭️ [매핑 ${i}] 이미 로드된 컬럼이 있음`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 소스 테이블이 선택되어 있으면 컬럼 로드
|
||||||
|
if (mapping.sourceTable) {
|
||||||
|
console.log(`📡 [매핑 ${i}] 소스 테이블 컬럼 자동 로드:`, mapping.sourceTable);
|
||||||
|
await loadMappingSourceColumns(mapping.sourceTable, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadSavedMappingColumns();
|
||||||
|
}, [config.parentDataMapping]);
|
||||||
|
|
||||||
// 2레벨 메뉴 목록 로드
|
// 2레벨 메뉴 목록 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadMenus = async () => {
|
const loadMenus = async () => {
|
||||||
|
|
@ -251,26 +308,39 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
|
|
||||||
// 메뉴 선택 시 카테고리 목록 로드
|
// 메뉴 선택 시 카테고리 목록 로드
|
||||||
const handleMenuSelect = async (menuObjid: number, fieldType: "discountType" | "roundingType" | "roundingUnit") => {
|
const handleMenuSelect = async (menuObjid: number, fieldType: "discountType" | "roundingType" | "roundingUnit") => {
|
||||||
if (!config.targetTable) {
|
console.log("🔍 [handleMenuSelect] 시작", { menuObjid, fieldType });
|
||||||
console.warn("⚠️ targetTable이 설정되지 않았습니다");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🔍 카테고리 목록 로드 시작", { targetTable: config.targetTable, menuObjid, fieldType });
|
// 🔧 1단계: 아코디언 먼저 열기 (리렌더링 전에)
|
||||||
|
setExpandedCategoryMappings(prev => {
|
||||||
|
const newState = { ...prev, [fieldType]: true };
|
||||||
|
console.log("🔄 [handleMenuSelect] 아코디언 열기:", newState);
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
|
||||||
const response = await getCategoryColumns(config.targetTable);
|
// 🔧 2단계: 메뉴별 카테고리 컬럼 API 호출
|
||||||
|
const response = await getCategoryColumnsByMenu(menuObjid);
|
||||||
|
|
||||||
console.log("📥 getCategoryColumns 응답:", response);
|
console.log("📥 [handleMenuSelect] API 응답:", response);
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
console.log("✅ 카테고리 컬럼 데이터:", response.data);
|
console.log("✅ [handleMenuSelect] 카테고리 컬럼 데이터:", {
|
||||||
setCategoryColumns(prev => ({ ...prev, [fieldType]: response.data }));
|
fieldType,
|
||||||
|
columns: response.data,
|
||||||
|
count: response.data.length
|
||||||
|
});
|
||||||
|
|
||||||
|
// 카테고리 컬럼 상태 업데이트
|
||||||
|
setCategoryColumns(prev => {
|
||||||
|
const newState = { ...prev, [fieldType]: response.data };
|
||||||
|
console.log("🔄 [handleMenuSelect] categoryColumns 업데이트:", newState);
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.error("❌ 카테고리 컬럼 로드 실패:", response);
|
console.error("❌ [handleMenuSelect] 카테고리 컬럼 로드 실패:", response);
|
||||||
}
|
}
|
||||||
|
|
||||||
// valueMapping 업데이트
|
// 🔧 3단계: valueMapping 업데이트 (마지막에)
|
||||||
handleChange("autoCalculation", {
|
const newConfig = {
|
||||||
...config.autoCalculation,
|
...config.autoCalculation,
|
||||||
valueMapping: {
|
valueMapping: {
|
||||||
...config.autoCalculation.valueMapping,
|
...config.autoCalculation.valueMapping,
|
||||||
|
|
@ -279,20 +349,50 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
[fieldType]: menuObjid,
|
[fieldType]: menuObjid,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
console.log("🔄 [handleMenuSelect] valueMapping 업데이트:", newConfig);
|
||||||
|
handleChange("autoCalculation", newConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 카테고리 선택 시 카테고리 값 목록 로드
|
// 카테고리 선택 시 카테고리 값 목록 로드
|
||||||
const handleCategorySelect = async (columnName: string, menuObjid: number, fieldType: "discountType" | "roundingType" | "roundingUnit") => {
|
const handleCategorySelect = async (columnName: string, menuObjid: number, fieldType: "discountType" | "roundingType" | "roundingUnit") => {
|
||||||
if (!config.targetTable) return;
|
console.log("🔍 [handleCategorySelect] 시작", { columnName, menuObjid, fieldType, targetTable: config.targetTable });
|
||||||
|
|
||||||
const response = await getCategoryValues(config.targetTable, columnName, false, menuObjid);
|
if (!config.targetTable) {
|
||||||
if (response.success && response.data) {
|
console.warn("⚠️ [handleCategorySelect] targetTable이 없습니다");
|
||||||
setCategoryValues(prev => ({ ...prev, [fieldType]: response.data }));
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const response = await getCategoryValues(config.targetTable, columnName, false, menuObjid);
|
||||||
|
|
||||||
|
console.log("📥 [handleCategorySelect] API 응답:", response);
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
console.log("✅ [handleCategorySelect] 카테고리 값 데이터:", {
|
||||||
|
fieldType,
|
||||||
|
values: response.data,
|
||||||
|
count: response.data.length
|
||||||
|
});
|
||||||
|
|
||||||
|
setCategoryValues(prev => {
|
||||||
|
const newState = { ...prev, [fieldType]: response.data };
|
||||||
|
console.log("🔄 [handleCategorySelect] categoryValues 업데이트:", newState);
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error("❌ [handleCategorySelect] 카테고리 값 로드 실패:", response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔧 카테고리 선택 시 아코디언 열기 (이미 열려있을 수도 있음)
|
||||||
|
setExpandedCategoryMappings(prev => {
|
||||||
|
const newState = { ...prev, [fieldType]: true };
|
||||||
|
console.log("🔄 [handleCategorySelect] 아코디언 상태:", newState);
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
|
||||||
// valueMapping 업데이트
|
// valueMapping 업데이트
|
||||||
handleChange("autoCalculation", {
|
const newConfig = {
|
||||||
...config.autoCalculation,
|
...config.autoCalculation,
|
||||||
valueMapping: {
|
valueMapping: {
|
||||||
...config.autoCalculation.valueMapping,
|
...config.autoCalculation.valueMapping,
|
||||||
|
|
@ -301,9 +401,99 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
[fieldType]: columnName,
|
[fieldType]: columnName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
console.log("🔄 [handleCategorySelect] valueMapping 업데이트:", newConfig);
|
||||||
|
handleChange("autoCalculation", newConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 🆕 저장된 설정에서 카테고리 정보 복원
|
||||||
|
useEffect(() => {
|
||||||
|
const loadSavedCategories = async () => {
|
||||||
|
console.log("🔍 [loadSavedCategories] useEffect 실행", {
|
||||||
|
hasTargetTable: !!config.targetTable,
|
||||||
|
hasAutoCalc: !!config.autoCalculation,
|
||||||
|
hasValueMapping: !!config.autoCalculation?.valueMapping
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!config.targetTable || !config.autoCalculation?.valueMapping) {
|
||||||
|
console.warn("⚠️ [loadSavedCategories] targetTable 또는 valueMapping이 없어 종료");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedMenus = (config.autoCalculation.valueMapping as any)?._selectedMenus;
|
||||||
|
const savedCategories = (config.autoCalculation.valueMapping as any)?._selectedCategories;
|
||||||
|
|
||||||
|
console.log("🔄 [loadSavedCategories] 저장된 카테고리 설정 복원 시작:", { savedMenus, savedCategories });
|
||||||
|
|
||||||
|
// 각 필드 타입별로 저장된 카테고리 값 로드
|
||||||
|
const fieldTypes: Array<"discountType" | "roundingType" | "roundingUnit"> = ["discountType", "roundingType", "roundingUnit"];
|
||||||
|
|
||||||
|
// 🔧 복원할 아코디언 상태 준비
|
||||||
|
const newExpandedState: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
for (const fieldType of fieldTypes) {
|
||||||
|
const menuObjid = savedMenus?.[fieldType];
|
||||||
|
const columnName = savedCategories?.[fieldType];
|
||||||
|
|
||||||
|
console.log(`🔍 [loadSavedCategories] ${fieldType} 처리`, { menuObjid, columnName });
|
||||||
|
|
||||||
|
// 🔧 메뉴만 선택된 경우에도 카테고리 컬럼 로드
|
||||||
|
if (menuObjid) {
|
||||||
|
console.log(`✅ [loadSavedCategories] ${fieldType} 메뉴 발견, 카테고리 컬럼 로드 시작:`, { menuObjid });
|
||||||
|
|
||||||
|
// 🔧 메뉴가 선택되어 있으면 아코디언 열기
|
||||||
|
newExpandedState[fieldType] = true;
|
||||||
|
|
||||||
|
// 🔧 메뉴별 카테고리 컬럼 로드 (카테고리 선택 여부와 무관)
|
||||||
|
console.log(`📡 [loadSavedCategories] ${fieldType} 카테고리 컬럼 API 호출`, { menuObjid });
|
||||||
|
const columnsResponse = await getCategoryColumnsByMenu(menuObjid);
|
||||||
|
console.log(`📥 [loadSavedCategories] ${fieldType} 컬럼 응답:`, columnsResponse);
|
||||||
|
|
||||||
|
if (columnsResponse.success && columnsResponse.data) {
|
||||||
|
setCategoryColumns(prev => {
|
||||||
|
const newState = { ...prev, [fieldType]: columnsResponse.data };
|
||||||
|
console.log(`🔄 [loadSavedCategories] ${fieldType} categoryColumns 업데이트:`, newState);
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error(`❌ [loadSavedCategories] ${fieldType} 컬럼 로드 실패:`, columnsResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔧 카테고리까지 선택된 경우에만 값 로드
|
||||||
|
if (columnName) {
|
||||||
|
console.log(`📡 [loadSavedCategories] ${fieldType} 카테고리 값 API 호출`, { columnName });
|
||||||
|
const valuesResponse = await getCategoryValues(config.targetTable, columnName, false, menuObjid);
|
||||||
|
console.log(`📥 [loadSavedCategories] ${fieldType} 값 응답:`, valuesResponse);
|
||||||
|
|
||||||
|
if (valuesResponse.success && valuesResponse.data) {
|
||||||
|
console.log(`✅ [loadSavedCategories] ${fieldType} 카테고리 값:`, valuesResponse.data);
|
||||||
|
setCategoryValues(prev => {
|
||||||
|
const newState = { ...prev, [fieldType]: valuesResponse.data };
|
||||||
|
console.log(`🔄 [loadSavedCategories] ${fieldType} categoryValues 업데이트:`, newState);
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error(`❌ [loadSavedCategories] ${fieldType} 값 로드 실패:`, valuesResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔧 저장된 설정이 있는 아코디언들 열기
|
||||||
|
if (Object.keys(newExpandedState).length > 0) {
|
||||||
|
console.log("🔓 [loadSavedCategories] 아코디언 열기:", newExpandedState);
|
||||||
|
setExpandedCategoryMappings(prev => {
|
||||||
|
const finalState = { ...prev, ...newExpandedState };
|
||||||
|
console.log("🔄 [loadSavedCategories] 최종 아코디언 상태:", finalState);
|
||||||
|
return finalState;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadSavedCategories();
|
||||||
|
}, [config.targetTable, config.autoCalculation?.valueMapping]);
|
||||||
|
|
||||||
// 🆕 초기 로드 시 screenTableName을 targetTable로 자동 설정
|
// 🆕 초기 로드 시 screenTableName을 targetTable로 자동 설정
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (screenTableName && !config.targetTable) {
|
if (screenTableName && !config.targetTable) {
|
||||||
|
|
@ -344,10 +534,37 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
|
|
||||||
// 필드 제거
|
// 필드 제거
|
||||||
const removeField = (index: number) => {
|
const removeField = (index: number) => {
|
||||||
|
// 로컬 입력 상태에서도 제거
|
||||||
|
setLocalFieldInputs(prev => {
|
||||||
|
const newInputs = { ...prev };
|
||||||
|
delete newInputs[index];
|
||||||
|
return newInputs;
|
||||||
|
});
|
||||||
handleFieldsChange(localFields.filter((_, i) => i !== index));
|
handleFieldsChange(localFields.filter((_, i) => i !== index));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 필드 수정
|
// 🆕 로컬 필드 입력 업데이트 (포커스 유지용)
|
||||||
|
const updateFieldLocal = (index: number, field: 'label' | 'placeholder', value: string) => {
|
||||||
|
setLocalFieldInputs(prev => ({
|
||||||
|
...prev,
|
||||||
|
[index]: {
|
||||||
|
...prev[index],
|
||||||
|
[field]: value
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🆕 실제 필드 데이터 업데이트 (onBlur 시 호출)
|
||||||
|
const handleFieldBlur = (index: number) => {
|
||||||
|
const localInput = localFieldInputs[index];
|
||||||
|
if (localInput) {
|
||||||
|
const newFields = [...localFields];
|
||||||
|
newFields[index] = { ...newFields[index], ...localInput };
|
||||||
|
handleFieldsChange(newFields);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 필드 수정 (Switch 같은 즉시 업데이트가 필요한 경우에만 사용)
|
||||||
const updateField = (index: number, updates: Partial<AdditionalFieldDefinition>) => {
|
const updateField = (index: number, updates: Partial<AdditionalFieldDefinition>) => {
|
||||||
const newFields = [...localFields];
|
const newFields = [...localFields];
|
||||||
newFields[index] = { ...newFields[index], ...updates };
|
newFields[index] = { ...newFields[index], ...updates };
|
||||||
|
|
@ -386,14 +603,30 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
handleFieldGroupsChange(localFieldGroups.filter(g => g.id !== groupId));
|
handleFieldGroupsChange(localFieldGroups.filter(g => g.id !== groupId));
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateFieldGroup = (groupId: string, updates: Partial<FieldGroup>) => {
|
// 🆕 로컬 그룹 입력 업데이트 (포커스 유지용)
|
||||||
// 1. 로컬 입력 상태 즉시 업데이트 (포커스 유지)
|
const updateGroupLocal = (groupId: string, field: 'id' | 'title' | 'description' | 'order', value: any) => {
|
||||||
setLocalGroupInputs(prev => ({
|
setLocalGroupInputs(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[groupId]: { ...prev[groupId], ...updates }
|
[groupId]: {
|
||||||
|
...prev[groupId],
|
||||||
|
[field]: value
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
};
|
||||||
// 2. 실제 그룹 데이터 업데이트
|
|
||||||
|
// 🆕 실제 그룹 데이터 업데이트 (onBlur 시 호출)
|
||||||
|
const handleGroupBlur = (groupId: string) => {
|
||||||
|
const localInput = localGroupInputs[groupId];
|
||||||
|
if (localInput) {
|
||||||
|
const newGroups = localFieldGroups.map(g =>
|
||||||
|
g.id === groupId ? { ...g, ...localInput } : g
|
||||||
|
);
|
||||||
|
handleFieldGroupsChange(newGroups);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFieldGroup = (groupId: string, updates: Partial<FieldGroup>) => {
|
||||||
|
// 2. 실제 그룹 데이터 업데이트 (Switch 같은 즉시 업데이트가 필요한 경우에만 사용)
|
||||||
const newGroups = localFieldGroups.map(g =>
|
const newGroups = localFieldGroups.map(g =>
|
||||||
g.id === groupId ? { ...g, ...updates } : g
|
g.id === groupId ? { ...g, ...updates } : g
|
||||||
);
|
);
|
||||||
|
|
@ -467,6 +700,12 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
return g;
|
return g;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 🔧 아이템 추가 시 해당 그룹의 아코디언을 열린 상태로 유지
|
||||||
|
setExpandedDisplayItems(prev => ({
|
||||||
|
...prev,
|
||||||
|
[groupId]: true
|
||||||
|
}));
|
||||||
|
|
||||||
setLocalFieldGroups(updatedGroups);
|
setLocalFieldGroups(updatedGroups);
|
||||||
handleChange("fieldGroups", updatedGroups);
|
handleChange("fieldGroups", updatedGroups);
|
||||||
};
|
};
|
||||||
|
|
@ -796,8 +1035,9 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px] sm:text-xs">라벨</Label>
|
<Label className="text-[10px] sm:text-xs">라벨</Label>
|
||||||
<Input
|
<Input
|
||||||
value={field.label}
|
value={localFieldInputs[index]?.label !== undefined ? localFieldInputs[index].label : field.label}
|
||||||
onChange={(e) => updateField(index, { label: e.target.value })}
|
onChange={(e) => updateFieldLocal(index, 'label', e.target.value)}
|
||||||
|
onBlur={() => handleFieldBlur(index)}
|
||||||
placeholder="필드 라벨"
|
placeholder="필드 라벨"
|
||||||
className="h-6 w-full text-[10px] sm:h-7 sm:text-xs"
|
className="h-6 w-full text-[10px] sm:h-7 sm:text-xs"
|
||||||
/>
|
/>
|
||||||
|
|
@ -821,8 +1061,9 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px] sm:text-xs">Placeholder</Label>
|
<Label className="text-[10px] sm:text-xs">Placeholder</Label>
|
||||||
<Input
|
<Input
|
||||||
value={field.placeholder || ""}
|
value={localFieldInputs[index]?.placeholder !== undefined ? localFieldInputs[index].placeholder : (field.placeholder || "")}
|
||||||
onChange={(e) => updateField(index, { placeholder: e.target.value })}
|
onChange={(e) => updateFieldLocal(index, 'placeholder', e.target.value)}
|
||||||
|
onBlur={() => handleFieldBlur(index)}
|
||||||
placeholder="입력 안내"
|
placeholder="입력 안내"
|
||||||
className="h-6 w-full text-[10px] sm:h-7 sm:text-xs"
|
className="h-6 w-full text-[10px] sm:h-7 sm:text-xs"
|
||||||
/>
|
/>
|
||||||
|
|
@ -1078,14 +1319,8 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
<Label className="text-[10px] sm:text-xs">그룹 ID</Label>
|
<Label className="text-[10px] sm:text-xs">그룹 ID</Label>
|
||||||
<Input
|
<Input
|
||||||
value={localGroupInputs[group.id]?.id !== undefined ? localGroupInputs[group.id].id : group.id}
|
value={localGroupInputs[group.id]?.id !== undefined ? localGroupInputs[group.id].id : group.id}
|
||||||
onChange={(e) => {
|
onChange={(e) => updateGroupLocal(group.id, 'id', e.target.value)}
|
||||||
const newValue = e.target.value;
|
onBlur={() => handleGroupBlur(group.id)}
|
||||||
setLocalGroupInputs(prev => ({
|
|
||||||
...prev,
|
|
||||||
[group.id]: { ...prev[group.id], id: newValue }
|
|
||||||
}));
|
|
||||||
updateFieldGroup(group.id, { id: newValue });
|
|
||||||
}}
|
|
||||||
className="h-7 text-xs sm:h-8 sm:text-sm"
|
className="h-7 text-xs sm:h-8 sm:text-sm"
|
||||||
placeholder="group_customer"
|
placeholder="group_customer"
|
||||||
/>
|
/>
|
||||||
|
|
@ -1096,14 +1331,8 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
<Label className="text-[10px] sm:text-xs">그룹 제목</Label>
|
<Label className="text-[10px] sm:text-xs">그룹 제목</Label>
|
||||||
<Input
|
<Input
|
||||||
value={localGroupInputs[group.id]?.title !== undefined ? localGroupInputs[group.id].title : group.title}
|
value={localGroupInputs[group.id]?.title !== undefined ? localGroupInputs[group.id].title : group.title}
|
||||||
onChange={(e) => {
|
onChange={(e) => updateGroupLocal(group.id, 'title', e.target.value)}
|
||||||
const newValue = e.target.value;
|
onBlur={() => handleGroupBlur(group.id)}
|
||||||
setLocalGroupInputs(prev => ({
|
|
||||||
...prev,
|
|
||||||
[group.id]: { ...prev[group.id], title: newValue }
|
|
||||||
}));
|
|
||||||
updateFieldGroup(group.id, { title: newValue });
|
|
||||||
}}
|
|
||||||
className="h-7 text-xs sm:h-8 sm:text-sm"
|
className="h-7 text-xs sm:h-8 sm:text-sm"
|
||||||
placeholder="거래처 정보"
|
placeholder="거래처 정보"
|
||||||
/>
|
/>
|
||||||
|
|
@ -1114,14 +1343,8 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
<Label className="text-[10px] sm:text-xs">그룹 설명 (선택사항)</Label>
|
<Label className="text-[10px] sm:text-xs">그룹 설명 (선택사항)</Label>
|
||||||
<Input
|
<Input
|
||||||
value={localGroupInputs[group.id]?.description !== undefined ? localGroupInputs[group.id].description : (group.description || "")}
|
value={localGroupInputs[group.id]?.description !== undefined ? localGroupInputs[group.id].description : (group.description || "")}
|
||||||
onChange={(e) => {
|
onChange={(e) => updateGroupLocal(group.id, 'description', e.target.value)}
|
||||||
const newValue = e.target.value;
|
onBlur={() => handleGroupBlur(group.id)}
|
||||||
setLocalGroupInputs(prev => ({
|
|
||||||
...prev,
|
|
||||||
[group.id]: { ...prev[group.id], description: newValue }
|
|
||||||
}));
|
|
||||||
updateFieldGroup(group.id, { description: newValue });
|
|
||||||
}}
|
|
||||||
className="h-7 text-xs sm:h-8 sm:text-sm"
|
className="h-7 text-xs sm:h-8 sm:text-sm"
|
||||||
placeholder="거래처 관련 정보를 입력합니다"
|
placeholder="거래처 관련 정보를 입력합니다"
|
||||||
/>
|
/>
|
||||||
|
|
@ -1133,14 +1356,8 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={localGroupInputs[group.id]?.order !== undefined ? localGroupInputs[group.id].order : (group.order || 0)}
|
value={localGroupInputs[group.id]?.order !== undefined ? localGroupInputs[group.id].order : (group.order || 0)}
|
||||||
onChange={(e) => {
|
onChange={(e) => updateGroupLocal(group.id, 'order', parseInt(e.target.value) || 0)}
|
||||||
const newValue = parseInt(e.target.value) || 0;
|
onBlur={() => handleGroupBlur(group.id)}
|
||||||
setLocalGroupInputs(prev => ({
|
|
||||||
...prev,
|
|
||||||
[group.id]: { ...prev[group.id], order: newValue }
|
|
||||||
}));
|
|
||||||
updateFieldGroup(group.id, { order: newValue });
|
|
||||||
}}
|
|
||||||
className="h-7 text-xs sm:h-8 sm:text-sm"
|
className="h-7 text-xs sm:h-8 sm:text-sm"
|
||||||
min="0"
|
min="0"
|
||||||
/>
|
/>
|
||||||
|
|
@ -1236,8 +1453,30 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
{/* 아이콘 설정 */}
|
{/* 아이콘 설정 */}
|
||||||
{item.type === "icon" && (
|
{item.type === "icon" && (
|
||||||
<Input
|
<Input
|
||||||
value={item.icon || ""}
|
value={
|
||||||
onChange={(e) => updateDisplayItemInGroup(group.id, itemIndex, { icon: e.target.value })}
|
localDisplayItemInputs[group.id]?.[itemIndex]?.value !== undefined
|
||||||
|
? localDisplayItemInputs[group.id][itemIndex].value
|
||||||
|
: item.icon || ""
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
setLocalDisplayItemInputs(prev => ({
|
||||||
|
...prev,
|
||||||
|
[group.id]: {
|
||||||
|
...prev[group.id],
|
||||||
|
[itemIndex]: {
|
||||||
|
...prev[group.id]?.[itemIndex],
|
||||||
|
value: newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
const localValue = localDisplayItemInputs[group.id]?.[itemIndex]?.value;
|
||||||
|
if (localValue !== undefined) {
|
||||||
|
updateDisplayItemInGroup(group.id, itemIndex, { icon: localValue });
|
||||||
|
}
|
||||||
|
}}
|
||||||
placeholder="Building"
|
placeholder="Building"
|
||||||
className="h-6 text-[9px] sm:text-[10px]"
|
className="h-6 text-[9px] sm:text-[10px]"
|
||||||
/>
|
/>
|
||||||
|
|
@ -1264,8 +1503,12 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
// 실제 상태 업데이트
|
}}
|
||||||
updateDisplayItemInGroup(group.id, itemIndex, { value: newValue });
|
onBlur={() => {
|
||||||
|
const localValue = localDisplayItemInputs[group.id]?.[itemIndex]?.value;
|
||||||
|
if (localValue !== undefined) {
|
||||||
|
updateDisplayItemInGroup(group.id, itemIndex, { value: localValue });
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="| , / , -"
|
placeholder="| , / , -"
|
||||||
className="h-6 text-[9px] sm:text-[10px]"
|
className="h-6 text-[9px] sm:text-[10px]"
|
||||||
|
|
@ -1312,8 +1555,12 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
// 실제 상태 업데이트
|
}}
|
||||||
updateDisplayItemInGroup(group.id, itemIndex, { label: newValue });
|
onBlur={() => {
|
||||||
|
const localValue = localDisplayItemInputs[group.id]?.[itemIndex]?.label;
|
||||||
|
if (localValue !== undefined) {
|
||||||
|
updateDisplayItemInGroup(group.id, itemIndex, { label: localValue });
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="라벨 (예: 거래처:)"
|
placeholder="라벨 (예: 거래처:)"
|
||||||
className="h-6 w-full text-[9px] sm:text-[10px]"
|
className="h-6 w-full text-[9px] sm:text-[10px]"
|
||||||
|
|
@ -1354,8 +1601,30 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
{/* 기본값 */}
|
{/* 기본값 */}
|
||||||
{item.emptyBehavior === "default" && (
|
{item.emptyBehavior === "default" && (
|
||||||
<Input
|
<Input
|
||||||
value={item.defaultValue || ""}
|
value={
|
||||||
onChange={(e) => updateDisplayItemInGroup(group.id, itemIndex, { defaultValue: e.target.value })}
|
localDisplayItemInputs[group.id]?.[itemIndex]?.value !== undefined
|
||||||
|
? localDisplayItemInputs[group.id][itemIndex].value
|
||||||
|
: item.defaultValue || ""
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
setLocalDisplayItemInputs(prev => ({
|
||||||
|
...prev,
|
||||||
|
[group.id]: {
|
||||||
|
...prev[group.id],
|
||||||
|
[itemIndex]: {
|
||||||
|
...prev[group.id]?.[itemIndex],
|
||||||
|
value: newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
const localValue = localDisplayItemInputs[group.id]?.[itemIndex]?.value;
|
||||||
|
if (localValue !== undefined) {
|
||||||
|
updateDisplayItemInGroup(group.id, itemIndex, { defaultValue: localValue });
|
||||||
|
}
|
||||||
|
}}
|
||||||
placeholder="미입력"
|
placeholder="미입력"
|
||||||
className="h-6 w-full text-[9px] sm:text-[10px]"
|
className="h-6 w-full text-[9px] sm:text-[10px]"
|
||||||
/>
|
/>
|
||||||
|
|
@ -1670,14 +1939,21 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
<Label className="text-[10px] font-semibold sm:text-xs">카테고리 값 매핑</Label>
|
<Label className="text-[10px] font-semibold sm:text-xs">카테고리 값 매핑</Label>
|
||||||
|
|
||||||
{/* 할인 방식 매핑 */}
|
{/* 할인 방식 매핑 */}
|
||||||
<Collapsible>
|
<Collapsible
|
||||||
|
open={expandedCategoryMappings.discountType}
|
||||||
|
onOpenChange={(open) => setExpandedCategoryMappings(prev => ({ ...prev, discountType: open }))}
|
||||||
|
>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="flex w-full items-center justify-between p-2 hover:bg-muted"
|
className="flex w-full items-center justify-between p-2 hover:bg-muted"
|
||||||
>
|
>
|
||||||
<span className="text-xs font-medium">할인 방식 연산 매핑</span>
|
<span className="text-xs font-medium">할인 방식 연산 매핑</span>
|
||||||
<ChevronDown className="h-4 w-4" />
|
{expandedCategoryMappings.discountType ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent className="space-y-2 pt-2">
|
<CollapsibleContent className="space-y-2 pt-2">
|
||||||
|
|
@ -1702,30 +1978,40 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 2단계: 카테고리 선택 */}
|
{/* 2단계: 카테고리 선택 */}
|
||||||
{(config.autoCalculation.valueMapping as any)?._selectedMenus?.discountType && (
|
{(() => {
|
||||||
<div className="space-y-1">
|
const hasSelectedMenu = !!(config.autoCalculation.valueMapping as any)?._selectedMenus?.discountType;
|
||||||
<Label className="text-[9px] sm:text-[10px]">2단계: 카테고리 선택</Label>
|
const columns = categoryColumns.discountType || [];
|
||||||
<Select
|
console.log("🎨 [렌더링] 2단계 카테고리 선택", {
|
||||||
value={(config.autoCalculation.valueMapping as any)?._selectedCategories?.discountType || ""}
|
hasSelectedMenu,
|
||||||
onValueChange={(value) => handleCategorySelect(
|
columns,
|
||||||
value,
|
columnsCount: columns.length,
|
||||||
(config.autoCalculation.valueMapping as any)._selectedMenus.discountType,
|
categoryColumnsState: categoryColumns
|
||||||
"discountType"
|
});
|
||||||
)}
|
return hasSelectedMenu ? (
|
||||||
>
|
<div className="space-y-1">
|
||||||
<SelectTrigger className="h-7 text-xs">
|
<Label className="text-[9px] sm:text-[10px]">2단계: 카테고리 선택</Label>
|
||||||
<SelectValue placeholder="카테고리 선택" />
|
<Select
|
||||||
</SelectTrigger>
|
value={(config.autoCalculation.valueMapping as any)?._selectedCategories?.discountType || ""}
|
||||||
<SelectContent>
|
onValueChange={(value) => handleCategorySelect(
|
||||||
{(categoryColumns.discountType || []).map((col: any) => (
|
value,
|
||||||
<SelectItem key={col.columnName} value={col.columnName}>
|
(config.autoCalculation.valueMapping as any)._selectedMenus.discountType,
|
||||||
{col.columnLabel || col.columnName}
|
"discountType"
|
||||||
</SelectItem>
|
)}
|
||||||
))}
|
>
|
||||||
</SelectContent>
|
<SelectTrigger className="h-7 text-xs">
|
||||||
</Select>
|
<SelectValue placeholder="카테고리 선택" />
|
||||||
</div>
|
</SelectTrigger>
|
||||||
)}
|
<SelectContent>
|
||||||
|
{columns.map((col: any) => (
|
||||||
|
<SelectItem key={col.columnName} value={col.columnName}>
|
||||||
|
{col.columnLabel || col.columnName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* 3단계: 값 매핑 */}
|
{/* 3단계: 값 매핑 */}
|
||||||
{(config.autoCalculation.valueMapping as any)?._selectedCategories?.discountType && (
|
{(config.autoCalculation.valueMapping as any)?._selectedCategories?.discountType && (
|
||||||
|
|
@ -1780,14 +2066,21 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
{/* 반올림 방식 매핑 */}
|
{/* 반올림 방식 매핑 */}
|
||||||
<Collapsible>
|
<Collapsible
|
||||||
|
open={expandedCategoryMappings.roundingType}
|
||||||
|
onOpenChange={(open) => setExpandedCategoryMappings(prev => ({ ...prev, roundingType: open }))}
|
||||||
|
>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="flex w-full items-center justify-between p-2 hover:bg-muted"
|
className="flex w-full items-center justify-between p-2 hover:bg-muted"
|
||||||
>
|
>
|
||||||
<span className="text-xs font-medium">반올림 방식 연산 매핑</span>
|
<span className="text-xs font-medium">반올림 방식 연산 매핑</span>
|
||||||
<ChevronDown className="h-4 w-4" />
|
{expandedCategoryMappings.roundingType ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent className="space-y-2 pt-2">
|
<CollapsibleContent className="space-y-2 pt-2">
|
||||||
|
|
@ -1890,14 +2183,21 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
{/* 반올림 단위 매핑 */}
|
{/* 반올림 단위 매핑 */}
|
||||||
<Collapsible>
|
<Collapsible
|
||||||
|
open={expandedCategoryMappings.roundingUnit}
|
||||||
|
onOpenChange={(open) => setExpandedCategoryMappings(prev => ({ ...prev, roundingUnit: open }))}
|
||||||
|
>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="flex w-full items-center justify-between p-2 hover:bg-muted"
|
className="flex w-full items-center justify-between p-2 hover:bg-muted"
|
||||||
>
|
>
|
||||||
<span className="text-xs font-medium">반올림 단위 값 매핑</span>
|
<span className="text-xs font-medium">반올림 단위 값 매핑</span>
|
||||||
<ChevronDown className="h-4 w-4" />
|
{expandedCategoryMappings.roundingUnit ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent className="space-y-2 pt-2">
|
<CollapsibleContent className="space-y-2 pt-2">
|
||||||
|
|
@ -2235,10 +2535,10 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
className="h-7 w-full justify-between text-xs font-normal"
|
className="h-7 w-full justify-between text-xs font-normal"
|
||||||
disabled={targetTableColumns.length === 0}
|
disabled={!config.targetTable || loadedTargetTableColumns.length === 0}
|
||||||
>
|
>
|
||||||
{mapping.targetField
|
{mapping.targetField
|
||||||
? targetTableColumns.find((c) => c.columnName === mapping.targetField)?.columnLabel ||
|
? loadedTargetTableColumns.find((c) => c.columnName === mapping.targetField)?.columnLabel ||
|
||||||
mapping.targetField
|
mapping.targetField
|
||||||
: "저장 테이블 컬럼 선택"}
|
: "저장 테이블 컬럼 선택"}
|
||||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||||
|
|
@ -2248,13 +2548,15 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
|
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
{targetTableColumns.length === 0 ? (
|
{!config.targetTable ? (
|
||||||
<CommandEmpty className="text-xs">저장 테이블을 먼저 선택하세요</CommandEmpty>
|
<CommandEmpty className="text-xs">저장 대상 테이블을 먼저 선택하세요</CommandEmpty>
|
||||||
|
) : loadedTargetTableColumns.length === 0 ? (
|
||||||
|
<CommandEmpty className="text-xs">컬럼 로딩 중...</CommandEmpty>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<CommandEmpty className="text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
<CommandEmpty className="text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{targetTableColumns.map((col) => {
|
{loadedTargetTableColumns.map((col) => {
|
||||||
const searchValue = `${col.columnLabel || col.columnName} ${col.columnName} ${col.dataType || ""}`.toLowerCase();
|
const searchValue = `${col.columnLabel || col.columnName} ${col.columnName} ${col.dataType || ""}`.toLowerCase();
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
|
|
@ -2276,7 +2578,9 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span>{col.columnLabel || col.columnName}</span>
|
<span>{col.columnLabel || col.columnName}</span>
|
||||||
{col.dataType && (
|
{col.dataType && (
|
||||||
<span className="text-[10px] text-muted-foreground">{col.dataType}</span>
|
<span className="text-[10px] text-muted-foreground">
|
||||||
|
{col.dataType}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
|
|
@ -2289,17 +2593,27 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
<p className="text-[8px] text-muted-foreground">
|
||||||
|
현재 화면의 저장 대상 테이블 ({config.targetTable || "미선택"})의 컬럼
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 기본값 (선택사항) */}
|
{/* 기본값 (선택사항) */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-[9px] sm:text-[10px]">기본값 (선택사항)</Label>
|
<Label className="text-[9px] sm:text-[10px]">기본값 (선택사항)</Label>
|
||||||
<Input
|
<Input
|
||||||
value={mapping.defaultValue || ""}
|
value={localMappingInputs[index] !== undefined ? localMappingInputs[index] : mapping.defaultValue || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const updated = [...(config.parentDataMapping || [])];
|
const newValue = e.target.value;
|
||||||
updated[index] = { ...updated[index], defaultValue: e.target.value };
|
setLocalMappingInputs(prev => ({ ...prev, [index]: newValue }));
|
||||||
handleChange("parentDataMapping", updated);
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
const currentValue = localMappingInputs[index];
|
||||||
|
if (currentValue !== undefined) {
|
||||||
|
const updated = [...(config.parentDataMapping || [])];
|
||||||
|
updated[index] = { ...updated[index], defaultValue: currentValue || undefined };
|
||||||
|
handleChange("parentDataMapping", updated);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="값이 없을 때 사용할 기본값"
|
placeholder="값이 없을 때 사용할 기본값"
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs"
|
||||||
|
|
@ -2307,46 +2621,24 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 삭제 버튼 */}
|
{/* 삭제 버튼 */}
|
||||||
<Button
|
<div className="flex justify-end pt-2">
|
||||||
size="sm"
|
<Button
|
||||||
variant="ghost"
|
size="sm"
|
||||||
className="h-6 w-full text-xs text-destructive hover:text-destructive"
|
variant="ghost"
|
||||||
onClick={() => {
|
className="h-7 text-xs text-destructive hover:bg-destructive/10 hover:text-destructive"
|
||||||
const updated = (config.parentDataMapping || []).filter((_, i) => i !== index);
|
onClick={() => {
|
||||||
handleChange("parentDataMapping", updated);
|
const updated = (config.parentDataMapping || []).filter((_, i) => i !== index);
|
||||||
}}
|
handleChange("parentDataMapping", updated);
|
||||||
>
|
}}
|
||||||
<X className="mr-1 h-3 w-3" />
|
>
|
||||||
삭제
|
<X className="mr-1 h-3 w-3" />
|
||||||
</Button>
|
삭제
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(config.parentDataMapping || []).length === 0 && (
|
|
||||||
<p className="text-center text-[10px] text-muted-foreground py-4">
|
|
||||||
매핑 설정이 없습니다. "추가" 버튼을 클릭하여 설정하세요.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 예시 */}
|
|
||||||
<div className="rounded-lg bg-green-50 p-2 text-xs">
|
|
||||||
<p className="mb-1 text-[10px] font-medium text-green-900">💡 예시</p>
|
|
||||||
<div className="space-y-1 text-[9px] text-green-700">
|
|
||||||
<p><strong>매핑 1: 거래처 ID</strong></p>
|
|
||||||
<p className="ml-2">• 소스 테이블: <code className="bg-green-100 px-1">customer_mng</code></p>
|
|
||||||
<p className="ml-2">• 원본 필드: <code className="bg-green-100 px-1">id</code> → 저장 필드: <code className="bg-green-100 px-1">customer_id</code></p>
|
|
||||||
|
|
||||||
<p className="mt-1"><strong>매핑 2: 품목 ID</strong></p>
|
|
||||||
<p className="ml-2">• 소스 테이블: <code className="bg-green-100 px-1">item_info</code></p>
|
|
||||||
<p className="ml-2">• 원본 필드: <code className="bg-green-100 px-1">id</code> → 저장 필드: <code className="bg-green-100 px-1">item_id</code></p>
|
|
||||||
|
|
||||||
<p className="mt-1"><strong>매핑 3: 품목 기준단가</strong></p>
|
|
||||||
<p className="ml-2">• 소스 테이블: <code className="bg-green-100 px-1">item_info</code></p>
|
|
||||||
<p className="ml-2">• 원본 필드: <code className="bg-green-100 px-1">standard_price</code> → 저장 필드: <code className="bg-green-100 px-1">base_price</code></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 사용 예시 */}
|
{/* 사용 예시 */}
|
||||||
|
|
@ -2363,3 +2655,5 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
};
|
};
|
||||||
|
|
||||||
SelectedItemsDetailInputConfigPanel.displayName = "SelectedItemsDetailInputConfigPanel";
|
SelectedItemsDetailInputConfigPanel.displayName = "SelectedItemsDetailInputConfigPanel";
|
||||||
|
|
||||||
|
export default SelectedItemsDetailInputConfigPanel;
|
||||||
|
|
|
||||||
|
|
@ -2404,18 +2404,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (() => {
|
) : groupByColumns.length > 0 && groupedData.length > 0 ? (
|
||||||
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;
|
|
||||||
})() ? (
|
|
||||||
// 그룹화된 렌더링
|
// 그룹화된 렌더링
|
||||||
groupedData.map((group) => {
|
groupedData.map((group) => {
|
||||||
console.log("📊 [TableList] 그룹 렌더링:", group.groupKey, group.count);
|
|
||||||
const isCollapsed = collapsedGroups.has(group.groupKey);
|
const isCollapsed = collapsedGroups.has(group.groupKey);
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={group.groupKey}>
|
<React.Fragment key={group.groupKey}>
|
||||||
|
|
@ -2508,10 +2499,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
// 일반 렌더링 (그룹 없음)
|
// 일반 렌더링 (그룹 없음)
|
||||||
(() => {
|
data.map((row, index) => (
|
||||||
console.log("📋 [TableList] 일반 렌더링 시작:", data.length, "개 행");
|
|
||||||
return data;
|
|
||||||
})().map((row, index) => (
|
|
||||||
<tr
|
<tr
|
||||||
key={index}
|
key={index}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { GroupingPanel } from "@/components/screen/table-options/GroupingPanel";
|
||||||
import { TableFilter } from "@/types/table-options";
|
import { TableFilter } from "@/types/table-options";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { ModernDatePicker } from "@/components/screen/filters/ModernDatePicker";
|
import { ModernDatePicker } from "@/components/screen/filters/ModernDatePicker";
|
||||||
|
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||||
|
|
||||||
interface PresetFilter {
|
interface PresetFilter {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -44,6 +45,7 @@ interface TableSearchWidgetProps {
|
||||||
|
|
||||||
export function TableSearchWidget({ component, screenId, onHeightChange }: TableSearchWidgetProps) {
|
export function TableSearchWidget({ component, screenId, onHeightChange }: TableSearchWidgetProps) {
|
||||||
const { registeredTables, selectedTableId, setSelectedTableId, getTable } = useTableOptions();
|
const { registeredTables, selectedTableId, setSelectedTableId, getTable } = useTableOptions();
|
||||||
|
const { isPreviewMode } = useScreenPreview(); // 미리보기 모드 확인
|
||||||
|
|
||||||
// 높이 관리 context (실제 화면에서만 사용)
|
// 높이 관리 context (실제 화면에서만 사용)
|
||||||
let setWidgetHeight:
|
let setWidgetHeight:
|
||||||
|
|
@ -445,14 +447,14 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 동적 모드일 때만 설정 버튼들 표시 */}
|
{/* 동적 모드일 때만 설정 버튼들 표시 (미리보기에서는 비활성화) */}
|
||||||
{filterMode === "dynamic" && (
|
{filterMode === "dynamic" && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setColumnVisibilityOpen(true)}
|
onClick={() => !isPreviewMode && setColumnVisibilityOpen(true)}
|
||||||
disabled={!selectedTableId}
|
disabled={!selectedTableId || isPreviewMode}
|
||||||
className="h-8 text-xs sm:h-9 sm:text-sm"
|
className="h-8 text-xs sm:h-9 sm:text-sm"
|
||||||
>
|
>
|
||||||
<Settings className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
<Settings className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
|
|
@ -462,8 +464,8 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setFilterOpen(true)}
|
onClick={() => !isPreviewMode && setFilterOpen(true)}
|
||||||
disabled={!selectedTableId}
|
disabled={!selectedTableId || isPreviewMode}
|
||||||
className="h-8 text-xs sm:h-9 sm:text-sm"
|
className="h-8 text-xs sm:h-9 sm:text-sm"
|
||||||
>
|
>
|
||||||
<Filter className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
<Filter className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
|
|
@ -473,8 +475,8 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setGroupingOpen(true)}
|
onClick={() => !isPreviewMode && setGroupingOpen(true)}
|
||||||
disabled={!selectedTableId}
|
disabled={!selectedTableId || isPreviewMode}
|
||||||
className="h-8 text-xs sm:h-9 sm:text-sm"
|
className="h-8 text-xs sm:h-9 sm:text-sm"
|
||||||
>
|
>
|
||||||
<Layers className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
<Layers className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
|
|
|
||||||
|
|
@ -378,3 +378,4 @@ interface TablePermission {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue