Compare commits

..

No commits in common. "6ef4ff8e9b028f5697be21ca22c41ad350b82abf" and "e8c02fef5e3c788162705d5c766d33ab9a7b2659" have entirely different histories.

28 changed files with 532 additions and 220 deletions

View File

@ -0,0 +1,19 @@
{
"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"
}

View File

@ -0,0 +1,16 @@
{
"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"
}

View File

@ -0,0 +1,19 @@
{
"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"
}

View File

@ -0,0 +1,16 @@
{
"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> \"이희진\" &lt;zian9227@naver.com&gt;</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"
}

View File

@ -0,0 +1,18 @@
{
"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"
}

View File

@ -0,0 +1,16 @@
{
"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"
}

View File

@ -0,0 +1,16 @@
{
"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"
}

View File

@ -0,0 +1,16 @@
{
"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"
}

View File

@ -0,0 +1,18 @@
{
"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"
}

View File

@ -0,0 +1,16 @@
{
"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"
}

View File

@ -0,0 +1,13 @@
{
"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"
}

View File

@ -0,0 +1,13 @@
{
"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"
}

View File

@ -0,0 +1,19 @@
{
"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"
}

View File

@ -0,0 +1,18 @@
{
"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"
}

View File

@ -0,0 +1,18 @@
{
"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"
}

View File

@ -0,0 +1,16 @@
{
"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"
}

View File

@ -0,0 +1,13 @@
{
"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"
}

View File

@ -0,0 +1,16 @@
{
"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"
}

View File

@ -0,0 +1,28 @@
{
"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"
}

View File

@ -99,19 +99,11 @@ export class DynamicFormService {
} }
try { try {
// YYYY-MM-DD 형식인 경우 // YYYY-MM-DD 형식인 경우 시간 추가해서 Date 객체 생성
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) { if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
// DATE 타입이면 문자열 그대로 유지 console.log(`📅 날짜 타입 변환: ${value} -> Date 객체`);
if (lowerDataType === "date") {
console.log(`📅 날짜 문자열 유지: ${value} -> "${value}" (DATE 타입)`);
return value; // 문자열 그대로 반환
}
// TIMESTAMP 타입이면 Date 객체로 변환
else {
console.log(`📅 날짜시간 변환: ${value} -> Date 객체 (TIMESTAMP 타입)`);
return new Date(value + "T00:00:00"); return new Date(value + "T00:00:00");
} }
}
// 다른 날짜 형식도 Date 객체로 변환 // 다른 날짜 형식도 Date 객체로 변환
else { else {
console.log(`📅 날짜 타입 변환: ${value} -> Date 객체`); console.log(`📅 날짜 타입 변환: ${value} -> Date 객체`);
@ -308,13 +300,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] = value; // 문자열 그대로 유지 (이미 올바른 형식) dataToInsert[key] = new Date(value + "T00:00:00");
} }
} }
}); });
@ -857,22 +849,10 @@ 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}${pkCast} WHERE ${primaryKeyColumn} = $${values.length}::text
RETURNING * RETURNING *
`; `;

View File

@ -356,6 +356,17 @@ 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 =
@ -395,13 +406,33 @@ 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";
@ -489,6 +520,12 @@ 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");
@ -567,6 +604,12 @@ 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");
@ -575,6 +618,7 @@ function ScreenViewPage() {
}} }}
refreshKey={tableRefreshKey} refreshKey={tableRefreshKey}
onRefresh={() => { onRefresh={() => {
console.log("🔄 테이블 새로고침 요청됨 (자식)");
setTableRefreshKey((prev) => prev + 1); setTableRefreshKey((prev) => prev + 1);
setSelectedRowsData([]); // 선택 해제 setSelectedRowsData([]); // 선택 해제
}} }}

View File

@ -64,9 +64,6 @@ 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);
@ -161,45 +158,6 @@ 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);
}
}; };
// 전체 금액 계산 // 전체 금액 계산
@ -380,7 +338,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={handleItemsChange} onChange={setSelectedItems}
/> />
</div> </div>

View File

@ -316,33 +316,6 @@ 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;
@ -360,17 +333,6 @@ 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) => {
@ -386,33 +348,24 @@ 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) {
// 날짜 필드인 경우 정규화
if (dateFields.includes(fieldName)) {
const normalizedDate = normalizeDateField(formData[fieldName]);
if (normalizedDate) {
insertData[fieldName] = normalizedDate;
console.log(`🔗 [공통 필드 - 날짜] ${fieldName} 값 추가:`, normalizedDate);
}
} else {
insertData[fieldName] = formData[fieldName]; insertData[fieldName] = formData[fieldName];
console.log(`🔗 [공통 필드] ${fieldName} 값 추가:`, formData[fieldName]); console.log(`🔗 [공통 필드] ${fieldName} 값 추가:`, formData[fieldName]);
} }
}
}); });
console.log("📦 [신규 품목] 최종 insertData:", insertData); console.log("📦 [신규 품목] 최종 insertData:", insertData);
@ -451,15 +404,8 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
} }
// 🆕 값 정규화 함수 (타입 통일) // 🆕 값 정규화 함수 (타입 통일)
const normalizeValue = (val: any, fieldName?: string): any => { const normalizeValue = (val: any): 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);
@ -476,14 +422,13 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
} }
// 🆕 타입 정규화 후 비교 // 🆕 타입 정규화 후 비교
const currentValue = normalizeValue(currentData[key], key); const currentValue = normalizeValue(currentData[key]);
const originalValue = normalizeValue(originalItemData[key], key); const originalValue = normalizeValue(originalItemData[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];
} }
}); });
@ -686,6 +631,13 @@ 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;

View File

@ -122,6 +122,10 @@ 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;
@ -129,15 +133,31 @@ 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();
@ -189,6 +209,7 @@ 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'
@ -203,6 +224,7 @@ 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);
} }
} }

View File

@ -289,8 +289,17 @@ 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] || "";
} }

View File

@ -13,6 +13,8 @@ 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를
@ -41,6 +43,11 @@ 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";
@ -79,8 +86,24 @@ 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) => {

View File

@ -195,57 +195,17 @@ 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) {
console.log("📤 외부 onChange 호출"); externalOnChange(newData);
externalOnChange(processedData);
} }
// 🆕 onFormDataChange 호출하여 EditModal의 groupData 업데이트 // 🆕 onFormDataChange 호출하여 EditModal의 groupData 업데이트
if (onFormDataChange && columnName) { if (onFormDataChange && columnName) {
console.log("📤 onFormDataChange 호출:", columnName); onFormDataChange(columnName, newData);
onFormDataChange(columnName, processedData);
} }
}; };
@ -259,19 +219,18 @@ 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, // 필드명을 라벨로 사용 (나중에 설정에서 변경 가능)
@ -279,72 +238,99 @@ 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("⚠️ [ModalRepeaterTable] columns와 sourceColumns 모두 비어있음!"); console.warn("⚠️ 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(`⚠️ [ModalRepeaterTable] sourceColumns 필터링: ${rawSourceColumns.length}개 → ${sourceColumns.length}`); console.warn(`⚠️ sourceColumns 필터링: ${rawSourceColumns.length}개 → ${sourceColumns.length} (빈 문자열 제거)`);
} }
if (rawUniqueField !== uniqueField) { if (rawUniqueField !== uniqueField) {
console.warn(`⚠️ [ModalRepeaterTable] uniqueField 자동 보정: "${rawUniqueField}" → "${uniqueField}"`); console.warn(`⚠️ uniqueField 자동 보정: "${rawUniqueField}" → "${uniqueField}"`);
} }
console.log("🎬 ModalRepeaterTableComponent 마운트:", {
columnsLength: columns.length,
sourceTable,
sourceColumns,
uniqueField,
});
if (columns.length === 0) { if (columns.length === 0) {
console.error("❌ [ModalRepeaterTable] columns가 비어있습니다!", { sourceColumns }); console.error("❌ columns가 비어있습니다! sourceColumns:", 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에 포함된 컬럼 제외 (조인된 컬럼 제거)
// 단, columnMappings에 정의된 컬럼은 저장해야 하므로 제외하지 않음 console.log("🔍 [ModalRepeaterTable] 필터링 전 데이터:", {
const mappedFields = columns sourceColumns,
.filter(col => col.mapping?.type === "source" && col.mapping?.sourceField) sourceTable,
.map(col => col.field); targetTable,
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 (key.startsWith("_")) {
return;
}
// sourceColumns에 포함되어 있지만 columnMappings에도 정의된 경우 → 저장함
if (mappedFields.includes(key)) {
filtered[key] = item[key];
return;
}
// sourceColumns에만 있고 매핑 안 된 경우 → 제외 (조인 전용)
if (sourceColumns.includes(key)) { if (sourceColumns.includes(key)) {
console.log(`${key} 제외 (sourceColumn)`);
return;
}
// 메타데이터 필드도 제외
if (key.startsWith("_")) {
console.log(`${key} 제외 (메타데이터)`);
return; return;
} }
// 나머지는 모두 저장
filtered[key] = item[key]; filtered[key] = item[key];
}); });
return filtered; return filtered;
}); });
// targetTable 메타데이터를 배열 항목에 추가 console.log("✅ [ModalRepeaterTable] 필터링 후 데이터:", {
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,
@ -352,19 +338,21 @@ 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] 저장 데이터 준비:", { console.log("✅ [ModalRepeaterTable] context.formData에 데이터 추가 완료:", {
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 호출 완료");
} }
}; };

View File

@ -2404,9 +2404,18 @@ 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}>
@ -2499,7 +2508,10 @@ 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(