Compare commits

...

1283 Commits

Author SHA1 Message Date
hjlee 147d187901 Merge pull request 'lhj' (#372) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/372
2026-01-19 17:26:05 +09:00
leeheejin d09a6977f7 검색필터 업그레이드 2026-01-19 17:25:12 +09:00
leeheejin faf4100056 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-19 13:17:10 +09:00
kjs 410b4a7b14 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node 2026-01-19 12:07:30 +09:00
kjs e4667cce5f refactor: 테이블 관리 서비스에서 쿼리 및 로깅 개선
- 다중 값 배열 검색 시 조건 처리 로직 개선
- 쿼리에서 main. 접두사 추가하여 명확한 테이블 참조 보장
- 불필요한 공백 제거 및 코드 가독성 향상
- 엔티티 관계 감지 로깅 개선으로 디버깅 용이성 증가
- 새로운 수주관리 및 거래처 테이블 추가로 멀티테넌시 지원 강화
2026-01-19 12:07:29 +09:00
leeheejin c282d5c611 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj 2026-01-19 10:14:48 +09:00
leeheejin d4afc06f4a Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-19 10:14:38 +09:00
leeheejin f2ab4f11bd 진짜 해결했음 진짜진짜로 2026-01-19 10:14:20 +09:00
hjlee 514d852fa6 Merge pull request '배포 다시..' (#371) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/371
2026-01-19 09:50:47 +09:00
leeheejin 8603fddbcb 배포 다시.. 2026-01-19 09:50:25 +09:00
hjlee 58adc0a100 Merge pull request 'lhj' (#370) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/370
2026-01-16 18:15:42 +09:00
leeheejin 0382c94d73 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-16 18:15:21 +09:00
leeheejin 49f67451eb 피벗수정오늘최종 2026-01-16 18:14:55 +09:00
DDD1542 e3852aca5d Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node 2026-01-16 17:41:21 +09:00
DDD1542 df8065503d feat: 메뉴 삭제 및 동기화 기능 개선
- 메뉴 삭제 시 하위 메뉴를 재귀적으로 수집하여 관련 데이터 정리 기능 추가
- 메뉴 삭제 성공 시 삭제된 메뉴와 하위 메뉴 수를 포함한 응답 메시지 개선
- 메뉴 복제 시 항상 활성화 상태로 설정
- 화면-메뉴 동기화 진행 상태를 사용자에게 알리기 위한 프로그레스 메시지 추가
- 최상위 회사 폴더는 메뉴로 생성하지 않고 스킵하는 로직 추가
- 동기화 진행 중 오버레이 UI 개선
2026-01-16 17:41:19 +09:00
hjlee 0a85146564 Merge pull request 'lhj' (#369) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/369
2026-01-16 17:40:11 +09:00
leeheejin ad3b853d04 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-16 17:39:55 +09:00
leeheejin 2a3cc7ba00 배포다시 되게 고쳐놓음 2026-01-16 17:39:35 +09:00
hjlee ee273c5103 Merge pull request 'lhj' (#368) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/368
2026-01-16 16:51:03 +09:00
leeheejin 50a25cb9de Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-16 16:50:34 +09:00
leeheejin d1631d15ff 안닫히게 수정 2 2026-01-16 16:49:59 +09:00
hjlee a020985630 Merge pull request 'lhj' (#367) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/367
2026-01-16 16:25:12 +09:00
leeheejin 351ecbb35d 배포에서 오류 안나게 수정 2026-01-16 16:24:43 +09:00
leeheejin d32e933c03 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-16 16:12:09 +09:00
leeheejin 4497985104 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj 2026-01-16 16:11:34 +09:00
kjs b97b0cc7d7 refactor: 화면 그룹 관련 API에서 AuthenticatedRequest 타입 사용
- 화면 그룹 목록 조회, 상세 조회, 생성, 수정, 삭제 API에서 Request 타입을 AuthenticatedRequest로 변경하여 사용자 인증 정보를 명확히 처리
- companyCode를 req.user?.companyCode || "*"로 설정하여 기본값 처리 개선
- 관련 API의 일관성 있는 타입 사용으로 코드 가독성 및 유지보수성 향상
2026-01-16 15:52:35 +09:00
hjlee 160ad87395 Merge pull request 'lhj' (#364) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/364
2026-01-16 15:18:32 +09:00
leeheejin 4972f26cee Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-16 15:18:09 +09:00
leeheejin 02eee979ea 고치기 완료 2026-01-16 15:17:49 +09:00
DDD1542 08de1372c5 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node 2026-01-16 14:48:21 +09:00
DDD1542 ab52c49492 feat: 화면 관리 기능 개선 (복제/삭제/그룹 관리/테이블 설정)
- 화면 관리 시스템의 복제, 삭제, 수정 및 테이블 설정 기능을 전면 개선
- 그룹 삭제 시 하위 그룹과의 연관성 정리 및 로딩 프로그레스 바 추가
- 화면 수정 기능 추가: 이름, 그룹, 역할, 정렬 순서 변경
- 테이블 설정 모달에 관련 기능 추가 및 데이터 일관성 유지
- 메뉴-화면 그룹 동기화 API 추가 및 관련 상태 관리 기능 구현
- 검색어 필터링 로직 개선: 다중 키워드 지원
- 관련 파일 및 진행 상태 업데이트
2026-01-16 14:48:15 +09:00
leeheejin 8a865ac1f4 이상한 부분 수정 피벗그리드 2026-01-16 14:29:19 +09:00
hjlee 0a89cc2fb0 Merge pull request '피벗' (#363) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/363
2026-01-16 14:03:31 +09:00
leeheejin ab3a493abb 피벗 2026-01-16 14:03:07 +09:00
hjlee ac0f461832 Merge pull request 'lhj' (#362) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/362
2026-01-16 10:19:35 +09:00
leeheejin c2256de8ec Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-16 10:19:05 +09:00
leeheejin 484c98da9e 피벗그리드 고침 2026-01-16 10:18:11 +09:00
DDD1542 b2dc06d0f2 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node 2026-01-15 17:36:41 +09:00
DDD1542 efa95af4b9 refactor: 코드 정리 및 불필요한 주석 제거
- EntityJoinController에서 중복 제거 설정 관련 주석 및 코드 삭제
- screenGroupController와 tableManagementController에서 AuthenticatedRequest 타입을 일반 Request로 변경
- 불필요한 로그 및 주석 제거로 코드 가독성 향상
- tableManagementController에서 에러 메시지 개선
2026-01-15 17:36:38 +09:00
SeongHyun Kim e8bdcbb95c fix: 발주/입고관리 그룹 편집 시 단건만 저장되던 문제 수정
EditModal.tsx: conditional-container 존재 시 onSave 미전달 로직 수정
ModalRepeaterTableComponent.tsx: groupedData prop 우선 사용하도록 변경
2026-01-15 17:05:01 +09:00
SeongHyun Kim 60ae073606 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node 2026-01-15 16:56:44 +09:00
SeongHyun Kim a36802ab10 fix: RepeaterFieldGroup 저장 경로에 채번 규칙 할당 로직 추가 2026-01-15 16:54:02 +09:00
hjlee 98c489ee22 Merge pull request '코드로 보이던 문제 최소한의 코드만 고침' (#360) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/360
2026-01-15 16:53:40 +09:00
leeheejin c77c6290d3 코드로 보이던 문제 최소한의 코드만 고침 2026-01-15 16:53:27 +09:00
gbpark 9dc549be09 Merge pull request 'feat: 화면 관리 기능 개선 (복제/삭제/그룹 관리)' (#359) from feature/v2-renewal into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/359
2026-01-15 14:59:21 +09:00
gbpark 40a226ca30 Merge branch 'main' into feature/v2-renewal 2026-01-15 14:59:12 +09:00
DDD1542 5d89b69451 feat: 화면 관리 기능 개선 (복제/삭제/그룹 관리)
- 단일 화면 복제 및 그룹 전체 복제 기능 추가
- 정렬 순서 유지 및 일괄 이름 변경 기능 구현
- 삭제 기능 개선: 단일 화면 삭제 및 그룹 삭제 시 옵션 추가
- 회사 코드 지원 기능 추가: 복제된 그룹/화면에 선택한 회사 코드 적용
- 관련 파일 및 진행 상태 업데이트
2026-01-15 14:58:12 +09:00
SeongHyun Kim 7fd3364aef Merge branch 'ksh' 2026-01-15 13:27:01 +09:00
hjlee 2326c3548b Merge pull request 'lhj' (#358) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/358
2026-01-15 12:08:46 +09:00
leeheejin 220ce57be1 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-15 12:07:51 +09:00
leeheejin 0ac83b1551 분할패널에서 수정해도 리스트에서 삭제가 안되던 문제 코드 해결 2026-01-15 12:07:26 +09:00
kjs 3f474ecddd 오류수정 2026-01-15 11:00:30 +09:00
kjs ddf5ed4006 빌드에러 2026-01-15 10:52:54 +09:00
kjs c4ee084a1d 빌드에러 수정 2026-01-15 10:47:58 +09:00
SeongHyun Kim 2e02ace388 feat(repeater): 하위 데이터 조회 및 조건부 입력 기능 구현, 테이블 선택 데이터 동기화 개선
Repeater 컴포넌트에 하위 데이터 조회 기능 추가 (재고/단가 조회)
조건부 입력 활성화 및 최대값 제한 기능 구현
필드 정의 순서 변경 기능 추가 (드래그앤드롭, 화살표 버튼)
TableListComponent의 DataProvider 클로저 문제 해결
ButtonPrimaryComponent에 modalDataStore fallback 로직 추가
2026-01-15 10:35:34 +09:00
kjs 435eb90763 Merge pull request 'feat/multilang' (#357) from feat/multilang into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/357
2026-01-14 18:28:20 +09:00
kjs 98870b3348 Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into feat/multilang 2026-01-14 17:14:42 +09:00
kjs b7b750d134 번역현황 표시 2026-01-14 17:14:27 +09:00
kjs ac334db0b1 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feat/multilang 2026-01-14 16:33:38 +09:00
kjs 16c9c71a23 다국어 화면에서 수정기능 구현 2026-01-14 16:33:22 +09:00
DDD1542 059ea6b30a feat: 테이블 설정 모달의 컬럼 저장 로직 개선
- 변경된 컬럼들만 저장하도록 로직을 개선하여 불필요한 API 호출을 줄임
- 기존 컬럼 정보와 편집된 값을 병합하여 최종 설정을 구성
- 엔티티 및 코드 타입에 대한 세부 설정 처리 로직 추가
- 저장 성공 시 사용자에게 피드백 제공 및 편집 상태 초기화 기능 구현
2026-01-14 16:09:00 +09:00
kjs 14f8714ea1 Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into feat/multilang 2026-01-14 15:39:04 +09:00
kjs a27cb85007 코드 정리 및 최적화: InteractiveScreenViewer 컴포넌트의 불필요한 공백 제거 및 Select 컴포넌트의 props 정리 2026-01-14 15:38:52 +09:00
kjs b5d2195cd5 Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into feat/multilang 2026-01-14 15:38:41 +09:00
DDD1542 0a3d42f3ad feat: 테이블 타입 관리 모달 추가 및 기존 테이블 생성 모달 제거
- 테이블 설정 모달에 테이블 타입 관리 모달을 추가하여 데이터베이스 테이블과 컬럼의 타입을 관리할 수 있도록 개선
- 기존의 테이블 생성 모달을 제거하고, 버튼 텍스트를 "새 테이블 생성"에서 "테이블 타입 관리"로 변경
- 테이블 타입 관리 모달의 헤더 및 설명 추가, 모달 닫기 버튼 기능 구현
- 테이블 데이터 새로고침 기능을 추가하여 모달 닫기 시 데이터가 최신 상태로 유지되도록 개선
2026-01-14 15:37:29 +09:00
kjs b5c2e85496 화면 다국어 처리 2026-01-14 15:33:57 +09:00
DDD1542 f321aaf7aa feat: 화면 디자이너 모달 및 제어 관리 탭 기능 추가
- 화면 설정 모달에 "제어 관리" 탭 추가하여 버튼 제어 설정을 간편하게 관리
- 버튼 액션 설정 기능 구현: 버튼 목록 표시 및 각 버튼의 액션 타입 수정 가능
- 화면 디자이너 모달 통합: 전체화면 Dialog 내부에 ScreenDesigner 임베드
- URL 쿼리 파라미터로 화면 디자이너 자동 열기 기능 추가
- 화면 캔버스 크기 자동 조절 기능 구현: 최소 크기 보장 및 여유 마진 추가
- 필드 추가/제거 기능 개선: 기존 그리드 컬럼 변경 로직과 통합하여 사용자 경험 향상
2026-01-14 14:35:27 +09:00
kjs 26bb93ab6e 다국어 생성후 매핑 자동저장 2026-01-14 13:26:41 +09:00
kjs f9575d7b5f 다국어 버튼 자동매핑 2026-01-14 13:08:44 +09:00
kjs c26b346054 다국어설정 모달생성 2026-01-14 11:51:24 +09:00
kjs 24315215de 다국어 키 자동생성 로직 2026-01-14 11:05:57 +09:00
SeongHyun Kim ca73685bc2 Merge remote-tracking branch 'origin/main' into ksh 2026-01-14 10:22:58 +09:00
kjs 61a7f585b4 다국어 자동생성 2026-01-14 10:20:27 +09:00
SeongHyun Kim cf97db7fbf feat(universal-form-modal): Select 필드 직접 입력(Combobox) 모드 추가
SelectOptionConfig에 allowCustomInput 옵션 추가
FieldDetailSettingsModal에 "직접 입력 허용" Switch UI 추가
CascadingSelectField에 Combobox 모드 구현 (Popover+Command)
SelectField에 Combobox 모드 구현
목록 선택과 직접 입력 동시 지원
2026-01-13 18:44:59 +09:00
kjs 18b5161398 Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into feat/multilang 2026-01-13 18:32:04 +09:00
kjs b576837f18 다국어 관리 시스템 개선: 카테고리 및 키 자동 생성 기능 추가 2026-01-13 18:28:11 +09:00
SeongHyun Kim ef27e0e38f feat(universal-form-modal): 연쇄 드롭다운(Cascading Dropdown) 기능 구현
- SelectOptionConfig에 cascading 타입 및 설정 객체 추가
- FieldDetailSettingsModal에 연쇄 드롭다운 설정 UI 구현
  - 부모 필드 선택 (섹션별 그룹핑 콤보박스)
  - 관계 코드 선택 시 상세 설정 자동 채움
  - 소스 테이블, 부모 키 컬럼, 값/라벨 컬럼 설정
- UniversalFormModalComponent에 자식 필드 초기화 로직 추가
- selectOptions.cascading 방식 CascadingSelectField 렌더링 지원
2026-01-13 18:26:41 +09:00
SeongHyun Kim d7d7dabe84 fix: Select 옵션의 참조 테이블 컬럼 자동 로드 추가
- FieldDetailSettingsModal에서 모달 열릴 때 Select 옵션의 참조 테이블 컬럼을 자동으로 로드하도록 useEffect 추가
- field.selectOptions.tableName이 설정되어 있고 해당 테이블의 컬럼이 로드되지 않은 경우 onLoadTableColumns 호출
- 기존 linkedFieldGroup의 sourceTable 로드 로직과 동일한 패턴 적용
2026-01-13 17:05:16 +09:00
hjlee d22fd078be Merge pull request 'lhj' (#356) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/356
2026-01-13 15:43:38 +09:00
leeheejin 28fe908704 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-13 15:43:24 +09:00
leeheejin 1b5ae5fe1c 일단 오류 수정 2026-01-13 15:43:04 +09:00
DDD1542 905a9f62c3 feat: 프리뷰 모드에서 회사 코드 오버라이드 기능 추가
- 최고 관리자만 다른 회사 코드로 오버라이드 가능하도록 로직 개선
- entityJoinController 및 tableManagementController에서 회사 코드 오버라이드 처리 추가
- 관련 API 호출 시 오버라이드된 회사 코드 적용
- 프리뷰 모드 감지 및 UI 개선을 위한 코드 추가
2026-01-13 13:28:50 +09:00
kjs 989b7e53a7 Merge pull request 'feature/screen-management' (#355) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/355
2026-01-13 10:25:20 +09:00
kjs 20e144af36 Merge branch 'main' into feature/screen-management 2026-01-13 10:25:08 +09:00
hjlee e2a22bb853 Merge pull request 'lhj' (#354) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/354
2026-01-13 10:04:41 +09:00
leeheejin 0deb466557 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-13 10:04:19 +09:00
leeheejin f64279d084 카테고리 연속입력 2026-01-13 10:04:05 +09:00
SeongHyun Kim c74e97d66e chore: update react-is to version 18.3.1 and downgrade recharts to version 3.2.1 in package.json and package-lock.json 2026-01-13 09:35:01 +09:00
kjs 0beb8b20a3 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2026-01-13 09:30:59 +09:00
hjlee 054da65a26 Merge pull request 'lhj' (#353) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/353
2026-01-13 09:30:27 +09:00
kjs 75e6c9eb1a 엑셀업로드 제어로직 개선 2026-01-13 09:30:19 +09:00
leeheejin 0f2d0bb053 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-13 09:30:11 +09:00
leeheejin 306de370f1 탭 값 필터링 2026-01-13 09:29:58 +09:00
kjs b6fefe2ebd 엑셀 업로드 로직 개선 2026-01-12 17:43:34 +09:00
SeongHyun Kim f799402564 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into ksh 2026-01-12 17:28:11 +09:00
leeheejin 033f5eaf7e Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-12 17:27:28 +09:00
leeheejin d094b58ebf Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj 2026-01-12 17:27:18 +09:00
hjlee 3fa57ad2ae Merge pull request 'lhj' (#352) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/352
2026-01-12 17:26:48 +09:00
SeongHyun Kim 821955cfac fix: 렉 구조 등록 카테고리 데이터 저장 방식 통일 및 중복 체크 수정
RackStructureComponent: context에 카테고리 코드 필드(floorCode, zoneCode 등) 추가
기존 데이터 조회 시 warehouse_id -> warehouse_code 컬럼명 수정
미리보기 생성 시 카테고리 코드로 저장하도록 변경
미리보기 테이블에서 카테고리 코드를 라벨로 변환하여 표시
buttonActions: 중복 체크 API 호출 시 filters -> search 파라미터로 변경
types: RackStructureContext 인터페이스에 카테고리 코드 필드 추가
2026-01-12 17:25:52 +09:00
leeheejin b358a46c33 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-12 17:25:29 +09:00
hjjeong b2add92abf Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node 2026-01-12 17:25:22 +09:00
leeheejin c2836a0209 일단 퇴사일 입력하면 필터링 적용되게 수정해놓음 2026-01-12 17:25:12 +09:00
hjjeong 472fc8633c 상단마스터정보+하단품목정보 수정시 오류 수정 2026-01-12 17:24:25 +09:00
kjs 4801ee5ca4 Merge pull request 'feature/screen-management' (#351) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/351
2026-01-12 17:20:23 +09:00
kjs 87189c792e 카테고리값 자동감지 2026-01-12 16:08:02 +09:00
kjs 9cc5bbbf05 엑셀 업로드 문제 수정 2026-01-12 13:53:57 +09:00
kjs 5f991db9c4 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2026-01-12 10:33:01 +09:00
kjs 9e7253a293 카테고리 라벨 보이지 않는 문제 수정 2026-01-12 10:32:41 +09:00
leeheejin 31e87e0bca Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-12 09:25:13 +09:00
DDD1542 0773989c74 feat: 데이터 흐름 조회 기능 개선 및 프리뷰 모드 추가
- 데이터 흐름 조회 API에 source_screen_id 파라미터 추가하여 특정 화면에서 시작하는 데이터 흐름만 조회 가능
- 화면 관리 페이지에서 선택된 그룹에 company_code 필드 추가하여 회사 코드 정보 포함
- 프리뷰 모드에서 URL 쿼리로 company_code를 받아와 데이터 조회 시 우선 사용하도록 로직 개선
- 화면 관계 흐름 및 서브 테이블 정보에서 company_code를 활용하여 필터링 및 시각화 개선
2026-01-09 18:26:37 +09:00
kjs 6732e7d969 Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2026-01-09 18:22:51 +09:00
kjs 35f83c1937 로그 정리 2026-01-09 18:22:50 +09:00
kjs 8aa6008351 Merge pull request 'feature/screen-management' (#350) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/350
2026-01-09 17:57:08 +09:00
kjs 47b61a9a35 Merge branch 'main' into feature/screen-management 2026-01-09 17:57:01 +09:00
kjs d22c2ec96e Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2026-01-09 17:56:49 +09:00
kjs 3677c77da0 단일테이블 엑셀 업로드 채번 구현 2026-01-09 17:56:48 +09:00
SeongHyun Kim c11e80a43c Merge remote-tracking branch 'origin/main' into ksh 2026-01-09 17:46:44 +09:00
SeongHyun Kim f8fb7d687e fix: SelectedItemsDetailInput 수정 모드에서 다중 레코드 로드 오류 수정
DynamicComponentRenderer에 selected-items-detail-input groupedData 전달 추가
SelectedItemsDetailInput에서 groupedData 우선 사용하도록 변경
ScreenModal editData 배열 처리 시 formData/selectedData 분리 저장
TextInput 수정 모드에서 채번 규칙 스킵 로직 추가
2026-01-09 17:42:33 +09:00
DDD1542 a6569909a2 feat: 저장 테이블 제외 조건 추가 및 포커싱 개선
- 저장 테이블 쿼리에 table-list와 체크박스가 활성화된 화면, openModalWithData 버튼이 있는 화면을 제외하는 조건 추가
- 화면 그룹 클릭 시 새 그룹 진입 시 포커싱 없이 시작하도록 로직 개선
- 관련 문서에 제외 조건 및 SQL 예시 추가
2026-01-09 17:03:00 +09:00
leeheejin 5c9dda6826 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-09 15:58:26 +09:00
leeheejin bcf512d2b5 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj 2026-01-09 15:58:16 +09:00
kjs 4d41cb40b6 Merge pull request '빌드에러 수정' (#349) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/349
2026-01-09 15:57:15 +09:00
kjs bf74dd0f92 Merge branch 'main' into feature/screen-management 2026-01-09 15:57:09 +09:00
kjs 85ae1c1521 빌드에러 수정 2026-01-09 15:56:56 +09:00
leeheejin 38455325dd Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-09 15:48:39 +09:00
kjs f493f8ac80 Merge pull request 'feature/screen-management' (#348) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/348
2026-01-09 15:46:41 +09:00
kjs 7fc341bca8 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2026-01-09 15:46:24 +09:00
kjs ba2a281245 엑셀 업로드 제어로직 설정 가능하도록 수정 2026-01-09 15:46:09 +09:00
kjs aa0698556e 엑셀 업로드,다운로드 기능 개선 2026-01-09 15:32:02 +09:00
leeheejin c76123a927 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-09 15:21:12 +09:00
leeheejin ba20a2bf42 피벗 테스트만 하면 됨 기능은 완료 2026-01-09 15:11:30 +09:00
SeongHyun Kim 23c9604672 Merge origin/main into ksh - resolve conflicts 2026-01-09 14:55:16 +09:00
SeongHyun Kim 64c6942de3 feat(split-panel-layout): 멀티 테이블 탭 기능 추가
AdditionalTabConfig 타입 정의 (우측 패널과 동일한 설정 구조)
설정 패널에 추가 탭 설정 UI 구현 (테이블, 조인키, 컬럼, 필터, 중복제거, 버튼)
컴포넌트에 탭 상태 관리 및 데이터 로딩 로직 추가
탭 바 UI 및 탭별 컨텐츠 렌더링 구현
기존 기능 유지 (탭 미사용 시 동일 동작)
2026-01-09 14:52:32 +09:00
leeheejin f07448ac17 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-09 14:41:57 +09:00
leeheejin d49883d25f 피벗 추가 2026-01-09 14:41:27 +09:00
hjjeong 217e390fe9 Merge pull request 'fix/split-panel-edit-group-records' (#347) from fix/split-panel-edit-group-records into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/347
2026-01-09 14:34:16 +09:00
hjjeong 363ef44586 Merge remote-tracking branch 'origin/main' into fix/split-panel-edit-group-records 2026-01-09 14:33:02 +09:00
hjjeong 48aa004a7f fix: SplitPanelLayout 수정 버튼 클릭 시 그룹 레코드 조회 개선
- 수정 버튼 클릭 시 groupByColumns 기준으로 모든 관련 레코드 조회
- search 대신 dataFilter(equals) 사용하여 정확 매칭 조회
- deduplication 명시적 비활성화로 모든 레코드 반환
- supplier_mng, customer_mng 등 회사별 데이터 테이블 DB 조인 강제 (캐시 미사용)
- entityJoinController에 deduplication 파라미터 처리 추가
- ScreenModal에서 배열 형태 editData 처리 지원
2026-01-09 14:11:51 +09:00
kjs ee3a648917 삭제버튼 제어 동작하지 않던 오류 수정 2026-01-09 13:43:14 +09:00
leeheejin 819a281df4 fix: 피벗그리드 컴포넌트 레지스트리 등록 누락 수정 2026-01-09 13:34:55 +09:00
leeheejin dd1d3bb44d Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-09 13:28:40 +09:00
leeheejin 52e6824e76 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj 2026-01-09 13:28:26 +09:00
kjs 80cf20e142 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2026-01-09 12:17:27 +09:00
hjlee abddb67a30 Merge pull request 'lhj' (#346) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/346
2026-01-09 11:52:42 +09:00
leeheejin a0a9253d2c Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-09 11:51:48 +09:00
leeheejin 222a00b8a9 그리드랑 노드에서 delete 가 where 입력했는데도 저장이 안되던 오류 해결 2026-01-09 11:51:35 +09:00
leeheejin e8516d9d6b feat: DELETE 노드 WHERE 조건에 소스 필드 선택 기능 추가
- 소스 필드 목록을 연결된 입력 노드에서 자동으로 로딩
- WHERE 조건에 소스 필드 선택 Combobox 추가
- 정적 값과 소스 필드 중 선택 가능
- 조건 변경 시 자동 저장 기능 추가
2026-01-09 11:44:14 +09:00
kjs 150a40e2a8 Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2026-01-09 11:21:17 +09:00
kjs cea3aa53ae 엑셀 업로드 분할테이블 지원 2026-01-09 11:21:16 +09:00
DDD1542 af4072cef1 feat: 저장 테이블 정보 및 애니메이션 기능 추가
- 화면 서브 테이블에서 저장 테이블 정보를 추출하는 쿼리 추가
- 저장 테이블 정보 구조를 TableNodeData 인터페이스에 통합
- 저장 테이블의 시각적 표현을 위한 애니메이션 효과 추가
- 필터링 및 참조 관계 뱃지 레이아웃 개선
- 테이블 높이 부드러운 애니메이션 및 스크롤 기능 구현
2026-01-09 11:19:30 +09:00
kjs a50222e7d5 Merge pull request '이미지 문제 수정' (#345) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/345
2026-01-09 09:55:48 +09:00
kjs 69711f4e4b Merge branch 'main' into feature/screen-management 2026-01-09 09:55:39 +09:00
kjs 2eccd1982c 이미지 문제 수정 2026-01-09 09:55:24 +09:00
hjjeong 0baffafac1 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node 2026-01-08 18:01:07 +09:00
hjjeong 910d070055 fix: SelectedItemsDetailInput 저장 시 NULL 레코드 생성 버그 수정 2026-01-08 17:59:27 +09:00
hjjeong 8f4c95d20d fix: SelectedItemsDetailInput 저장 시 NULL 레코드 생성 버그 수정
- beforeFormSave 이벤트에 skipDefaultSave 플래그 추가
- UPSERT 처리 시 기본 저장 로직 건너뛰기
- 빈 엔트리 필터링으로 불필요한 레코드 생성 방지
2026-01-08 17:40:50 +09:00
kjs 65e1c1a995 Merge pull request '분할패널 수정버튼 동작하게 수정' (#344) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/344
2026-01-08 17:37:15 +09:00
kjs d2c15d519d 분할패널 수정버튼 동작하게 수정 2026-01-08 17:36:40 +09:00
hjlee 583c6c8c79 Merge pull request 'lhj' (#343) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/343
2026-01-08 17:15:03 +09:00
leeheejin a52ab0b206 refactor: pivot-grid 컴포넌트 개선 2026-01-08 17:11:46 +09:00
leeheejin 551e893f15 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-08 17:10:04 +09:00
leeheejin 85f8637ce0 fix: 채번 규칙 할당 로직 개선 - 복사 시 품목코드 자동생성 수정 2026-01-08 17:06:28 +09:00
leeheejin b85b3cd578 그리드? 일단 추가랑 복사기능 되게 했음 2026-01-08 17:05:27 +09:00
DDD1542 b8c8b31033 feat: 서브 테이블 조인 및 필터 관계 정보 개선
- rightPanel.columns에서 참조하는 외부 테이블 및 조인 컬럼 정보를 수집하는 로직 추가
- 서브 테이블의 조인 컬럼 참조 정보를 포함하여 시각화 개선
- 필터 관계를 선 없이 뱃지로 표시하여 겹침 방지 및 시각적 표현 개선
- TableNodeData 인터페이스에 조인 컬럼 및 참조 정보 필드 추가
- 화면 관계 흐름에서 조인 컬럼 및 필터 관계 정보 표시 기능 개선
2026-01-08 16:20:51 +09:00
kjs 0f57309d74 Merge pull request '분할패널 설정변경' (#342) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/342
2026-01-08 15:56:30 +09:00
kjs 4dfa82d3dd 분할패널 설정변경 2026-01-08 15:56:06 +09:00
SeongHyun Kim 34e48993e4 feat(SplitPanelLayout2): 개별 수정 버튼에 모달 화면 선택 UI 추가
- 좌측/우측 패널의 개별 수정 버튼 설정에 수정 모달 화면 선택 Combobox 추가
- 수정 버튼 ON 시 모달 화면 선택 UI 표시
- editModalScreenId 설정값 저장 및 사용
- 기존 폴백 로직 유지 (editModalScreenId 없으면 addModalScreenId 사용)
2026-01-08 15:37:22 +09:00
kjs 9821afe9cd Merge pull request 'feature/screen-management' (#341) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/341
2026-01-08 14:49:45 +09:00
kjs 38599a1bef Merge branch 'main' into feature/screen-management 2026-01-08 14:49:39 +09:00
kjs 11e25694b9 엔티티타입 입력 셀렉트박스 다중선택 기능 2026-01-08 14:49:24 +09:00
DDD1542 8928d851ca feat: 서브 테이블 정보 및 관계 시각화 개선
- 화면 서브 테이블에서 valueField, parentFieldId, cascadingParentField, controlField 정보를 추출하는 쿼리 추가
- 서브 테이블의 관계 유형을 추론하기 위한 추가 정보 필드(originalRelationType, foreignKey, leftColumn) 포함
- 필터링에 사용되는 FK 컬럼을 TableNodeData 인터페이스에 추가하여 시각화 개선
- 관계 유형별 색상 정의 및 시각적 관계 유형 추론 함수 추가
- 화면 관계 흐름에서 서브 테이블 연결선 및 필터링 참조 정보 표시 기능 개선
2026-01-08 14:24:33 +09:00
kjs 3f81c449ad 코드병합기능 개선 2026-01-08 14:24:07 +09:00
hjjeong 00006bf2e2 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node 2026-01-08 14:15:53 +09:00
hjjeong 3e9bf29bcf fix: SplitPanelLayout 그룹 삭제 시 groupByColumns 기준 삭제 및 멀티테넌시 보호 추가(영업관리_거래처별 품목 등록 등에서,,) 2026-01-08 14:13:19 +09:00
SeongHyun Kim 34ac1b0c42 Merge remote-tracking branch 'origin/main' into ksh 2026-01-08 14:06:07 +09:00
leeheejin df94d73662 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-08 12:34:39 +09:00
leeheejin dc449f6c69 글씨크기 조정 2026-01-08 12:34:19 +09:00
kjs dcf3a63d9b Merge pull request 'feature/screen-management' (#340) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/340
2026-01-08 12:33:15 +09:00
kjs a3c83c834e 드래그핸들 기능 수정 2026-01-08 12:32:50 +09:00
kjs 980c929d83 리도,언도기능 2026-01-08 12:28:48 +09:00
kjs a146667615 드래그 핸들 수정 2026-01-08 12:25:52 +09:00
SeongHyun Kim 2645d627da fix: 수주관리 수정 모달 저장 오류 수정 - UPDATE 폴백 로직 추가, 데이터 병합 순서 수정, 삭제 파라미터 순서 수정, id 타입 비교 통일 2026-01-08 12:25:35 +09:00
kjs f33d989202 복사된 셀 표시 2026-01-08 12:23:00 +09:00
kjs 6a1343b847 복사 붙여넣기 기능 2026-01-08 12:14:04 +09:00
kjs b61cb17aea 자동 채우기 핸들 2026-01-08 12:04:31 +09:00
kjs 83eb92cb27 엑셀 업로드 단계 통합 2026-01-08 11:51:02 +09:00
kjs 5321ea5b80 엑셀 업로드 템플릿 기능 구현 2026-01-08 11:45:39 +09:00
kjs d90a403ed9 엑셀 업로드 카테고리 타입 자동 감지 2026-01-08 11:09:40 +09:00
kjs c181385f11 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2026-01-08 10:40:01 +09:00
kjs 23ebae95d6 검색필터 틀고정기능 오류 수정 2026-01-08 10:39:48 +09:00
SeongHyun Kim 17498b1b2b refactor: UniversalFormModalComponent 자체 저장 기능 제거
saveSingleRow, saveWithCustomApi, handleSave, handleReset 함수 삭제
saving 상태 및 저장/초기화 버튼 UI 삭제
설정 패널에서 저장 버튼 관련 설정 UI 삭제
ModalConfig 타입에서 버튼 관련 속성 삭제
저장 처리는 button-primary (action: save)로 위임
약 468줄 코드 삭제
2026-01-08 10:04:05 +09:00
SeongHyun Kim 384106dd95 Merge remote-tracking branch 'origin/main' into ksh 2026-01-07 18:27:00 +09:00
SeongHyun Kim 6f4c9b7fdd ix: 부모-자식 모달 데이터 전달 문제 해결 및 미사용 multiRowSave 기능 제거
InteractiveScreenViewerDynamic: 생성 모드에서 formData를 initialData로 전달하도록 수정
UniversalFormModal: saveMultipleRows 함수 및 multiRowSave 관련 코드 전체 제거
types/config: MultiRowSaveConfig 인터페이스 및 기본값 제거
FieldDetailSettingsModal: receiveFromParent UI 옵션 제거
SaveSettingsModal: 저장 모드 설명 개선
DB: multiRowSave.enabled=true인 화면 3개 설정 수정
2026-01-07 17:42:40 +09:00
kjs 26c61ee5b6 Merge pull request 'feature/screen-management' (#339) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/339
2026-01-07 16:11:15 +09:00
kjs d8ff49d1db Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2026-01-07 16:11:02 +09:00
kjs 8c525673ab Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2026-01-07 16:10:12 +09:00
kjs 47ac9ecd8a 범용폼모달 외부소스 지원 2026-01-07 16:10:11 +09:00
hjlee 1b633e55d2 Merge pull request 'lhj' (#338) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/338
2026-01-07 16:07:50 +09:00
DDD1542 b279f8d58d feat: 화면 관리 기능 개선 및 서브 테이블 정보 추가
- 화면 선택 시 그룹을 재설정하지 않도록 로직 개선하여 데이터 재로드 방지
- 테이블 노드 데이터 구조에 필드 매핑 정보 추가
- 서브 테이블과 조인 관계를 시각화하기 위한 컬럼 강조 및 스타일링 개선
- 화면 관계 흐름에서 서브 테이블 연결선 강조 기능 추가
- 사용 컬럼 및 조인 컬럼 정보를 화면별로 매핑하여 관리
2026-01-07 14:50:03 +09:00
DDD1542 48e9840fa0 feat: 화면 그룹 생성 및 업데이트 기능 개선
- 화면 그룹 생성 시 회사 코드 결정 로직 추가: 최고 관리자가 특정 회사를 선택할 수 있도록 변경
- 부모 그룹이 있는 경우 그룹 레벨 및 계층 경로 계산 로직 추가
- 화면 그룹 생성 후 계층 경로 업데이트 기능 구현
- 화면 그룹 업데이트 시 불필요한 코드 제거 및 최적화
- 프론트엔드에서 화면 선택 시 그룹 및 서브 테이블 정보 연동 기능 개선
2026-01-07 14:49:49 +09:00
leeheejin 62226918a7 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-07 14:32:23 +09:00
leeheejin df70538027 pop화면 2026-01-07 14:31:04 +09:00
kjs 26020a29a0 Merge pull request 'feature/screen-management' (#337) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/337
2026-01-07 13:31:14 +09:00
kjs 52df163fbb Merge branch 'main' into feature/screen-management 2026-01-07 13:31:07 +09:00
kjs 777429af48 Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2026-01-07 13:30:57 +09:00
kjs 856db80a36 빌드 에러수정 2026-01-07 13:30:57 +09:00
kjs cd7adce874 Merge pull request '테이블에 존재하는지 확인하는 제어 추가' (#336) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/336
2026-01-07 11:55:53 +09:00
kjs ca260aa260 Merge branch 'main' into feature/screen-management 2026-01-07 11:55:45 +09:00
SeongHyun Kim 42d1a3fc5e Merge remote-tracking branch 'origin/main' into ksh 2026-01-07 10:28:47 +09:00
SeongHyun Kim 7c165a724e feat: 출고관리 수정 모달 저장 기능 개선 및 그룹화 컬럼 설정 UI 추가
ButtonConfigPanel: 수정 액션에 그룹화 컬럼 선택 드롭다운 추가 (영문/한글 검색 지원)
ScreenSplitPanel/EmbeddedScreen: groupedData prop 전달 경로 추가
buttonActions: RepeaterFieldGroup 저장 시 공통 필드 우선 적용되도록 병합 순서 변경
2026-01-07 10:24:01 +09:00
hjjeong 0ce0860dcc Merge pull request '입고 처리 시 재고 테이블 저장 및 재고이력 stock_id 전달 수정' (#335) from feature/inbound-inventory-fix into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/335
2026-01-07 10:06:37 +09:00
hjjeong c6ff839e54 입고 처리 시 재고 테이블 저장 및 재고이력 stock_id 전달 수정 2026-01-07 10:05:32 +09:00
kjs e308fd0ccc 테이블에 존재하는지 확인하는 제어 추가 2026-01-07 09:55:19 +09:00
hjlee f2cb7d14ca Merge pull request 'lhj' (#334) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/334
2026-01-07 09:48:46 +09:00
leeheejin b5b229122b Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-07 09:37:16 +09:00
leeheejin 126da9b46f 하이큐마그 점검항목 추가 후 주기명/점검방법명 오류 해결 2026-01-07 09:37:02 +09:00
SeongHyun Kim c365f06ed7 Merge remote-tracking branch 'origin/main' into ksh 2026-01-07 09:06:29 +09:00
hjlee 563081fa1c Merge pull request 'lhj' (#333) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/333
2026-01-06 17:57:31 +09:00
leeheejin 24331687d4 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-06 17:57:05 +09:00
leeheejin ea848b97ee 검색필터 업데이트 2026-01-06 17:56:31 +09:00
hjlee 15fc166683 Merge pull request 'lhj' (#332) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/332
2026-01-06 17:40:25 +09:00
leeheejin 26fdab5b4e Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-06 17:39:53 +09:00
leeheejin 12d3419b7f 구분 필터링 업데이트 2026-01-06 17:39:36 +09:00
SeongHyun Kim a2b701a4bf feat: 조건부 컨테이너 initialData 전달 체계 구현
InteractiveScreenViewerDynamic: originalData를 initialData로 추가 전달
DynamicComponentRenderer: initialData 우선순위 로직 추가
ConditionalContainerComponent: initialData props 추가 및 하위 전달
ConditionalSectionViewer: initialData props 추가 및 하위 전달
types.ts: initialData 타입 정의 추가
수정 모드에서 조건부 컨테이너 내부 컴포넌트 초기값 표시 지원
2026-01-06 17:29:41 +09:00
kjs 2213ad51b2 Merge pull request '수정' (#331) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/331
2026-01-06 17:02:54 +09:00
kjs 7120d5edc3 수정 2026-01-06 17:02:42 +09:00
kjs 0eb005ce35 Merge pull request '창고코드 같이 올라가게 수정' (#330) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/330
2026-01-06 15:34:02 +09:00
kjs 4828488c72 Merge branch 'main' into feature/screen-management 2026-01-06 15:33:55 +09:00
kjs c1425be57f 창고코드 같이 올라가게 수정 2026-01-06 15:33:44 +09:00
SeongHyun Kim 25b7e637de feat: 테이블 데이터 저장 시 존재하지 않는 컬럼 자동 필터링
- tableManagementService.addTableData: 테이블 스키마 기반 컬럼 필터링 로직 추가
- 무시된 컬럼 정보를 API 응답에 포함 (skippedColumns, savedColumns)
- 프론트엔드 콘솔에 무시된 컬럼 경고 출력
- conditional-container의 UI 제어용 필드(condition) 등으로 인한 저장 에러 방지
2026-01-06 15:29:26 +09:00
SeongHyun Kim ad39374e54 Merge remote-tracking branch 'origin/main' into ksh 2026-01-06 15:04:36 +09:00
SeongHyun Kim 77bb917248 feat: RepeaterFieldGroup 상위 폼 필드 전달 방식 개선
- 하드코딩된 masterDetailFields 배열을 규칙 기반 필터링으로 변경
- 제외 규칙: comp_ 접두사, _numberingRuleId 접미사, 배열/객체 타입, 빈 값 등
- 새 필드 추가 시 코드 수정 불필요하도록 개선
- 에러 로깅 상세 정보 추가 (status, data, message, fullError)
2026-01-06 15:03:22 +09:00
hjjeong 6bf914d9b1 Merge pull request '거래처 품목정보 거래처품번/단가 입력 없이 저장되도록' (#329) from fix/daejin-bugs into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/329
2026-01-06 15:02:38 +09:00
hjjeong e08c50c771 거래처 품목정보 거래처품번/단가 입력 없이 저장되도록 2026-01-06 15:01:50 +09:00
hjlee 0f027f2382 Merge pull request 'lhj' (#328) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/328
2026-01-06 14:45:07 +09:00
leeheejin 09d574fb8a Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-06 14:44:23 +09:00
leeheejin 6ae0778b4c 필터의 라벨도 코드말고 설정한걸로 나오게 수정 2026-01-06 14:43:57 +09:00
kjs 58b0e1b79b Merge pull request '범용 폼모달 라벨로 뜨게 수정' (#327) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/327
2026-01-06 14:24:52 +09:00
kjs f0322a49ad 범용 폼모달 라벨로 뜨게 수정 2026-01-06 14:24:30 +09:00
kjs 5e27d21257 Merge pull request 'feature/screen-management' (#326) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/326
2026-01-06 13:46:00 +09:00
kjs efc9175fec Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2026-01-06 13:45:48 +09:00
kjs eb61506acd 분할패널 수정동작 수정 2026-01-06 13:43:47 +09:00
SeongHyun Kim 75b5530d04 Merge remote-tracking branch 'origin/main' into ksh 2026-01-06 13:23:00 +09:00
kjs cded99d644 로그 제거 2026-01-06 13:08:33 +09:00
SeongHyun Kim 40fd5f9055 feat: 채번규칙 editable 옵션 수동 모드 감지 기능 구현
모달 오픈 시 채번 미리보기 원본값 저장 (numberingOriginalValues)
handleFieldChange에서 원본값 비교하여 수동/자동 모드 전환
사용자 수정 시 ruleId 제거하여 저장 시 채번 스킵
원본값 복구 시 ruleId 복구하여 자동 모드 복원
handleSave에서 채번 할당 조건 분기 처리
2026-01-06 13:06:28 +09:00
kjs 0709b8df25 Merge pull request '범용 폼 모달 사전필터 기능 수정' (#325) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/325
2026-01-06 11:43:38 +09:00
kjs 6bfc1a97a3 범용 폼 모달 사전필터 기능 수정 2026-01-06 11:43:26 +09:00
kjs 9ea0f1b84f Merge pull request 'feature/screen-management' (#324) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/324
2026-01-06 10:28:30 +09:00
kjs 7cb8026979 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2026-01-06 10:28:17 +09:00
kjs 4f77c38207 권한기능 임시 비활성화 2026-01-06 10:27:54 +09:00
leeheejin 68017ed0e9 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-06 10:21:07 +09:00
hjjeong 338c885cfa Merge pull request 'fix/daejin-bugs' (#323) from fix/daejin-bugs into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/323
2026-01-06 09:20:44 +09:00
SeongHyun Kim b3ee2b50e8 fix: 카테고리 Select 필드 저장 시 라벨값 대신 코드값 저장되도록 수정
- UniversalFormModalComponent.tsx: 카테고리 옵션 value를 valueLabel에서 valueCode로 변경
- 제어 로직 조건 비교 정상화 및 500 에러 해결
2026-01-05 18:41:49 +09:00
hjjeong 64105bf525 발주관리 수정시 공급처 표시 오류 수정 2026-01-05 18:21:29 +09:00
DDD1542 6925e3af3f feat: 화면 서브 테이블 정보 조회 기능 추가
- 화면 그룹에 대한 서브 테이블 관계를 조회하는 API 및 라우트 구현
- 화면 그룹 목록에서 서브 테이블 정보를 포함하여 데이터 흐름을 시각화
- 프론트엔드에서 화면 선택 시 그룹 및 서브 테이블 정보 연동 기능 추가
- 화면 노드 및 관계 시각화 컴포넌트에 서브 테이블 정보 통합
2026-01-05 18:18:26 +09:00
hjjeong 714698c20f 구매관리_발주관리 저장 시 마스터정보 품목에 전달 2026-01-05 17:08:47 +09:00
hjjeong 2a7066b6fd 테이블에 존재하는 컬럼만 업데이트 2026-01-05 17:08:03 +09:00
leeheejin 5fbc76f85d Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-05 14:47:26 +09:00
SeongHyun Kim e747162058 Merge remote-tracking branch 'origin/main' into ksh 2026-01-05 13:58:56 +09:00
SeongHyun Kim 914f3d57f3 fix: TableList 카테고리 라벨 표시를 위한 멀티테넌시 fallback 로직 추가
getColumnInputTypes API에서 회사별 설정이 없을 때 기본설정() fallback 적용
table_type_columns, category_column_mapping 조회 시 DISTINCT ON + ORDER BY CASE WHEN 패턴 사용
영향 범위: 모든 TableList 컴포넌트의 카테고리 컬럼 표시
2026-01-05 13:58:13 +09:00
kjs 12baad75c9 Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2026-01-05 13:54:42 +09:00
kjs 85519e302f 행선택시에만 버튼 활성화 기능 수정 2026-01-05 13:54:41 +09:00
kjs 98e96a1fb0 Merge pull request '즉시저장 액션 필수항목 체크 추가' (#322) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/322
2026-01-05 13:38:09 +09:00
kjs 76d7d5149b Merge branch 'main' into feature/screen-management 2026-01-05 13:38:02 +09:00
kjs 239e4800c7 즉시저장 액션 필수항목 체크 추가 2026-01-05 13:37:39 +09:00
leeheejin 6c75adb61d Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-05 10:10:23 +09:00
DDD1542 7caf2dea94 feat: 화면 그룹 관리 기능 추가
- 화면 그룹 CRUD API 및 라우트 구현
- 화면 그룹 목록 조회, 생성, 수정, 삭제 기능 추가
- 화면-그룹 연결 및 데이터 흐름 관리 기능 포함
- 프론트엔드에서 화면 그룹 필터링 및 시각화 기능
2026-01-05 10:05:31 +09:00
SeongHyun Kim ad76bfe3b0 Merge remote-tracking branch 'origin/main' into ksh 2026-01-05 09:28:12 +09:00
SeongHyun Kim 4ad58ba942 feat: 폼 모달 자동 채번 기능 구현 및 임베디드 스크린 렌더링 최적화
UniversalFormModalComponent에 폼 초기화 시 채번 규칙을 가져와 적용하는 generateNumberingValues 구현
채번 생성 중복 호출 방지를 위한 useRef 로직 추가
데이터 업데이트 시 불필요한 리마운트 및 포커스 분실을 방지하기 위해 EmbeddedScreen 컴포넌트 key에서 formDataVersion 제거
2026-01-04 17:41:07 +09:00
DDD1542 36a723b1a0 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node 2026-01-01 02:29:57 +09:00
DDD1542 5a94afc1d5 기존 회사변경수정 2026-01-01 02:29:53 +09:00
leeheejin 2889e4c82c Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2025-12-31 14:53:06 +09:00
kjs c15ec8f7b9 Merge pull request 'feature/screen-management' (#321) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/321
2025-12-31 14:17:53 +09:00
kjs eb868965df 조건부 계산식 2025-12-31 14:17:39 +09:00
kjs 417e1d297b 폼 조건별 계산식 설정기능 2025-12-31 13:53:30 +09:00
leeheejin 0b1dc98e5c Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2025-12-31 11:01:18 +09:00
kjs fff10a1911 Merge pull request '범용 폼모달 제어로직 연동' (#320) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/320
2025-12-31 10:54:28 +09:00
kjs 2842930dba Merge branch 'main' into feature/screen-management 2025-12-31 10:54:20 +09:00
kjs 5bdc903b0d 범용 폼모달 제어로직 연동 2025-12-31 10:54:07 +09:00
kjs 4ce0411809 Merge pull request 'feature/screen-management' (#319) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/319
2025-12-30 17:45:50 +09:00
kjs bd49db16c6 1 2025-12-30 17:45:38 +09:00
kjs 113ef24bdf Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-30 15:51:26 +09:00
kjs 7d6bff49aa 폼 채번 오작동 문제 수정 2025-12-30 15:36:28 +09:00
DDD1542 7b773f57b4 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node 2025-12-30 15:28:13 +09:00
DDD1542 58233e51de 각각 별도 TSX 병합 및 회사선택기능 추가 2025-12-30 15:28:05 +09:00
kjs 4421ccaa71 Merge pull request 'feature/screen-management' (#318) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/318
2025-12-30 14:19:44 +09:00
kjs 183f68e89a Merge branch 'main' into feature/screen-management 2025-12-30 14:19:36 +09:00
kjs fb82d2f5a1 분할패널에서 버튼 비활성화기능 수정 2025-12-30 14:19:15 +09:00
kjs 84a3956b02 버튼 버그 수정 2025-12-30 14:13:26 +09:00
kjs c78326bae1 버튼 비활성화 로직 2025-12-30 14:11:42 +09:00
kjs b45f4870e8 신규등록 버튼 버그 수정 2025-12-30 14:03:29 +09:00
kjs fd58e9cce2 행추가,모달 동시입력기능 구현 2025-12-30 13:32:49 +09:00
kjs c7efe8ec33 Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-30 12:33:18 +09:00
kjs 3c4e251e9b 폼 다중테이블 저장 지원 2025-12-30 12:33:17 +09:00
kjs e902987e44 Merge pull request '로그테이블 관련 내용 추가' (#317) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/317
2025-12-30 10:54:31 +09:00
kjs 54ca51258c Merge branch 'main' into feature/screen-management 2025-12-30 10:54:23 +09:00
kjs 06d5069566 로그테이블 관련 내용 추가 2025-12-30 10:54:06 +09:00
kjs e1d6c1740f Merge pull request '테이블 생성규칙' (#316) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/316
2025-12-30 10:49:01 +09:00
kjs c32bd8a4bf 테이블 생성규칙 2025-12-30 10:48:11 +09:00
leeheejin e040b94a62 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2025-12-30 09:41:56 +09:00
DDD1542 6476a83d86 변경추가 2025-12-29 18:19:17 +09:00
DDD1542 ea3b6d2083 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node 2025-12-29 17:58:27 +09:00
DDD1542 87caa4b3ca 커밋 메세지 메뉴별 대중소 정리 2025-12-29 17:56:26 +09:00
hyeonsu e30b1cc01a Merge pull request '마지막 merge' (#315) from reportMng into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/315
2025-12-29 17:53:24 +09:00
dohyeons 89ce2a9cd0 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into reportMng 2025-12-29 17:52:39 +09:00
SeongHyun Kim ef991b3b26 feat(universal-form-modal): 테이블 컬럼 저장 설정 및 참조 표시 기능 구현
컬럼별 저장 여부 설정 (saveToTarget: true/false)
저장 안 함 컬럼: 참조 ID로 소스 테이블 조회하여 표시만 함
수정 모드에서 참조 컬럼 값 자동 조회 (loadReferenceColumnValues)
Select 컴포넌트 빈 값 필터링으로 안정성 개선
조건 탭 변경 시 소스 데이터 즉시 로드
컬럼 필드 선택 안 함 옵션 추가 (표시 전용 컬럼)
2025-12-29 17:42:30 +09:00
leeheejin 22e0ce1fc5 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2025-12-29 15:54:07 +09:00
SeongHyun Kim 00376202fd feat(universal-form-modal): 조건부 테이블, 동적 Select 옵션, 서브 테이블 수정 로드 기능 구현
- 조건부 테이블: 체크박스/탭으로 조건 선택 시 다른 테이블 데이터 관리
- 동적 Select 옵션: 소스 테이블에서 드롭다운 옵션 동적 로드
- 행 선택 모드: Select 값 변경 시 같은 소스 행의 연관 컬럼 자동 채움
- 수정 모드 서브 테이블 로드: loadOnEdit 옵션으로 반복 섹션 데이터 자동 로드
- SplitPanelLayout2 메인 테이블 병합: 서브 테이블 수정 시 메인 데이터 함께 조회
- 연결 필드 그룹 표시 형식: subDisplayColumn 추가로 메인/서브 컬럼 분리 설정
- UX 개선: 컬럼 선택 UI를 검색 가능한 Combobox로 전환
- saveMainAsFirst 로직 개선: items 없어도 메인 데이터 저장 가능
2025-12-29 09:06:07 +09:00
SeongHyun Kim 6365ce4921 Merge remote-tracking branch 'origin/main' into ksh 2025-12-29 09:05:54 +09:00
SeongHyun Kim 47b23d1aa3 feat(universal-form-modal): 조건부 테이블, 동적 Select 옵션, 서브 테이블 수정 로드 기능 구현
조건부 테이블: 체크박스/탭으로 조건 선택 시 다른 테이블 데이터 관리
동적 Select 옵션: 소스 테이블에서 드롭다운 옵션 동적 로드
행 선택 모드: Select 값 변경 시 같은 소스 행의 연관 컬럼 자동 채움
수정 모드 서브 테이블 로드: loadOnEdit 옵션으로 반복 섹션 데이터 자동 로드
SplitPanelLayout2 메인 테이블 병합: 서브 테이블 수정 시 메인 데이터 함께 조회
연결 필드 그룹 표시 형식: subDisplayColumn 추가로 메인/서브 컬럼 분리 설정
UX 개선: 컬럼 선택 UI를 검색 가능한 Combobox로 전환
saveMainAsFirst 로직 개선: items 없어도 메인 데이터 저장 가능
2025-12-28 19:32:13 +09:00
kjs f63399c1e1 Merge pull request '2레벨메뉴 복사오류 해결' (#314) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/314
2025-12-24 18:38:15 +09:00
kjs 7ece757d3d Merge branch 'main' into feature/screen-management 2025-12-24 18:38:08 +09:00
kjs 722b4787e2 2레벨메뉴 복사오류 해결 2025-12-24 18:37:53 +09:00
SeongHyun Kim 486e5ee29b feat(SplitPanelLayout2): 좌우 패널 수정/삭제 기능 및 모달 자동 닫기 구현
- 좌측 패널에 수정/삭제 버튼 기능 추가
- 좌측 패널 설정에 개별 수정/삭제 UI 추가
- 삭제 API 호출을 백엔드 라우트에 맞게 수정 (DELETE /tables/{tableName}/delete)
- UniversalFormModal 저장 완료 후 closeEditModal 이벤트 발생하여 모달 자동 닫기
- ModalConfig에 showSaveButton 타입 추가
2025-12-24 14:01:38 +09:00
SeongHyun Kim 171ed6e938 fix: 생성 모드에서 부모 데이터가 UniversalFormModal에 전달되지 않는 문제 수정
- DynamicComponentRenderer에서 _initialData 전달 시 빈 객체 체크 추가
- 생성 모드(isCreateMode)에서 originalData가 빈 객체일 때 formData를 사용하도록 수정
- 부모 화면(SplitPanelLayout2)에서 전달한 dept_code, dept_name이 모달에서 정상 수신됨
2025-12-24 13:11:52 +09:00
dohyeons c20e393a1a 텍스트 인라인 편집 기능 추가 2025-12-24 10:58:41 +09:00
dohyeons f300b637d1 복제 및 스타일 복사 기능 추가 2025-12-24 10:42:34 +09:00
dohyeons 386ce629ac 다중 선택 컴포넌트 동시 이동 및 잠금 컴포넌트 복사 제한 2025-12-24 10:20:21 +09:00
dohyeons a299195b42 다중 선택 후 함께 이동하는 기능을 구현 2025-12-24 10:16:37 +09:00
dohyeons 352f9f441f 드래그 영역 선택(Marquee Selection) 기능 추가 2025-12-24 10:10:52 +09:00
dohyeons aa283d11da 텍스트 컴포넌트 더블 클릭 시 컨텐츠 크기에 맞게 자동 조절 2025-12-24 09:48:37 +09:00
leeheejin 6bd25c8a9e Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2025-12-24 09:25:29 +09:00
SeongHyun Kim 9878f1f502 fix(select): Radix UI Select v2.x value="" 에러 수정
Radix UI Select v2.0부터 빈 문자열 value=""가 금지됨 (placeholder 예약어)

수정 파일:
- FieldDetailSettingsModal.tsx: saveColumn "__default__"
- TableLogViewer.tsx: 전체 필터 "__all__"
- FlowStepPanel.tsx: disabled placeholder "__placeholder__"
- MapConfigPanel.tsx: 선택 안 함 "__none__" (2곳)
- DataMappingSettings.tsx: disabled placeholder "__placeholder__" (2곳)
- ScreenAssignmentTab.tsx: disabled placeholder "__placeholder__"
- multilang/page.tsx: 전체 메뉴/타입 "__all__" (2곳)
2025-12-24 09:24:56 +09:00
SeongHyun Kim 3396834417 feat(split-panel-layout2): 그룹핑, 탭 필터링, 설정 모달 기능 추가
- types.ts: GroupingConfig, TabConfig, ColumnDisplayConfig 등 타입 확장
- Component: groupData, generateTabs, filterDataByTab 함수 추가
- ConfigPanel: SearchableColumnSelect, 설정 모달 상태 관리 추가
- 신규 모달: ActionButtonConfigModal, ColumnConfigModal, DataTransferConfigModal
- UniversalFormModal: 연결필드 소스 테이블 Combobox로 변경
2025-12-24 09:08:16 +09:00
SeongHyun Kim 718788110a Merge remote-tracking branch 'origin/main' into ksh 2025-12-24 09:01:04 +09:00
hyeonsu ed2e0a1c6b Merge pull request '수정사항 반영' (#313) from reportMng into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/313
2025-12-23 17:38:05 +09:00
dohyeons 9fe22bc422 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into reportMng 2025-12-23 17:37:37 +09:00
dohyeons 859d68fff8 인쇄 기능 개선 - 중복 호출 제거 및 레이아웃 정확도 향상 2025-12-23 17:37:22 +09:00
kjs a7edd74574 Merge pull request 'feature/screen-management' (#312) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/312
2025-12-23 17:32:43 +09:00
kjs 755bbc0c58 복사 진짜진짜 수정 2025-12-23 17:32:27 +09:00
leeheejin 67471b2518 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2025-12-23 17:20:32 +09:00
kjs 542c0bae94 복사 원본데이터 참조 오류 수정 2025-12-23 17:06:21 +09:00
dohyeons 82a7ff62ee 서명 밑줄 옵션 완전히 제거 2025-12-23 16:00:25 +09:00
dohyeons 83f171189b 페이지 사이즈 변경 시 컴포넌트 위치/크기 비율 자동 조정 2025-12-23 15:12:21 +09:00
dohyeons 050a183c96 feat(report): 리포트-메뉴 연결 기능 추가 2025-12-23 14:34:49 +09:00
dohyeons e1567d3f77 워드 export 레이아웃 및 바코드/서명 렌더링 개선 2025-12-23 13:56:15 +09:00
dohyeons da195200a8 UX 개선 - 입력값 검증 및 confirm을 모달로 변경 2025-12-23 09:49:44 +09:00
kjs c910572754 Merge pull request 'feature/screen-management' (#311) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/311
2025-12-23 09:40:34 +09:00
kjs 4187ec0745 Merge branch 'main' into feature/screen-management 2025-12-23 09:40:26 +09:00
kjs 73cc969bd8 분할패널 상단헤더 크기 조정기능 2025-12-23 09:37:40 +09:00
kjs 5f406fbe88 공통코드 계층구조 구현 2025-12-23 09:31:18 +09:00
SeongHyun Kim 533eaf5c9f feat(TableSection): 테이블 컬럼 부모값 받기 기능 추가
TableColumnConfig에 receiveFromParent, parentFieldName 속성 추가
allComponents에서 부모 화면 필드 자동 추출
컬럼 설정에 "부모값" 스위치 및 부모 필드 선택 UI 추가
handleAddItems()에서 부모값 자동 적용 로직 구현
2025-12-23 09:24:59 +09:00
dohyeons 7875d8ab86 페이지 크기에 최솟값 1 설정 2025-12-22 18:20:16 +09:00
dohyeons e1a032933d 화면 여백에 최솟값 0 설정 2025-12-22 18:17:58 +09:00
dohyeons 99c0960325 서명 생성 시 폰트가 일부 글자에만 적용되는 문제 수정 2025-12-22 17:42:35 +09:00
hyeonsu ae6d917ec4 Merge pull request '리포트 관리 수정' (#310) from reportMng into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/310
2025-12-22 17:08:15 +09:00
dohyeons 5f26e998e3 워터마크를 전체 페이지 공유 방식으로 변경 2025-12-22 17:06:11 +09:00
kjs b85b888007 Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-22 16:39:47 +09:00
kjs 9493d81903 카테고리 복사에러 수정 2025-12-22 16:39:46 +09:00
dohyeons d7f015b37d Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into reportMng 2025-12-22 15:45:35 +09:00
dohyeons e8b581f5da 미리보기 모달 페이지 크기 불일치 수정 2025-12-22 15:45:17 +09:00
dohyeons d90e68905e 워터마크 기능 추가 2025-12-22 15:40:31 +09:00
leeheejin f1c4891924 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2025-12-22 15:31:17 +09:00
dohyeons 002c71f9e8 서명 생성 시 한글 폰트가 일부 글자만 표시되는 문제 수정 2025-12-22 15:21:14 +09:00
dohyeons 117912045f 캔버스 스케일 팩터를 고정 px 단위로 통일 2025-12-22 15:13:49 +09:00
SeongHyun Kim 6a4ebf362c feat(UniversalFormModal): 저장 버튼 표시 설정 옵션 추가
ConfigPanel에 showSaveButton 체크박스 추가
체크 해제 시 모달 하단 저장 버튼 숨김 가능
SaveSettingsModal SelectItem key 중복 해결
서브 테이블 삭제 버튼 클릭 이벤트 충돌 수정
2025-12-22 14:36:13 +09:00
dohyeons 0decfe95de 미리보기/인쇄에 바코드, QR코드, 체크박스 렌더링 추가 2025-12-22 13:58:12 +09:00
dohyeons 2b912105a8 QR코드 다중 필드 JSON 및 모든 행 포함 기능 추가 2025-12-22 13:36:42 +09:00
dohyeons acc867e38d QR코드 정사각형 강제 2025-12-22 11:51:19 +09:00
dohyeons c5cb4336e5 바코드/QR코드 투명 배경 처리 및 QR코드 에러 복구 버그 수정 2025-12-22 11:29:35 +09:00
kjs 01778661ed Merge pull request '복사에러 수정' (#309) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/309
2025-12-22 09:53:53 +09:00
kjs 6fced32e29 Merge branch 'main' into feature/screen-management 2025-12-22 09:53:45 +09:00
kjs 1cadafea0e 복사에러 수정 2025-12-22 09:53:22 +09:00
kjs 9162e3aa96 Merge pull request 'feature/screen-management' (#308) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/308
2025-12-22 09:30:18 +09:00
kjs 79b3c19c68 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-22 09:29:49 +09:00
kjs 43ae8d1c49 Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-19 18:31:21 +09:00
kjs a8bc7983c0 복사에러 수정 2025-12-19 18:31:20 +09:00
dohyeons 506a31df02 컴포넌트 WYSIWYG 개선 및 구분선 리사이즈 방향 제한 2025-12-19 18:24:18 +09:00
dohyeons 8789b2b864 구분선 리사이즈 개선 2025-12-19 18:19:29 +09:00
dohyeons 8d34b73a45 체크박스 컴포넌트 추가 2025-12-19 18:06:25 +09:00
dohyeons ea01309158 리포트 디자이너에 바코드/QR코드 컴포넌트 추가 2025-12-19 17:59:54 +09:00
kjs 45749c99c8 Merge pull request '채번 복사 오류 수정' (#307) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/307
2025-12-19 17:42:01 +09:00
kjs 43a6fb675f Merge branch 'main' into feature/screen-management 2025-12-19 17:41:55 +09:00
kjs 961e7e9a14 채번 복사 오류 수정 2025-12-19 17:41:14 +09:00
SeongHyun Kim f38447be8e Merge origin/main into ksh - resolve conflicts 2025-12-19 16:38:12 +09:00
SeongHyun Kim a1b05b8982 feat(UniversalFormModal): 수정 모드 INSERT/UPDATE/DELETE 지원
_groupedData를 테이블 섹션에 초기화하여 기존 품목 표시
originalGroupedData로 원본 데이터 보관하여 변경 추적
handleUniversalFormModalTableSectionSave()에 INSERT/UPDATE/DELETE 분기 로직 구현
EditModal, ConditionalSectionViewer에서 UniversalFormModal 감지 시 onSave 미전달
저장 완료 후 closeEditModal 이벤트 발생
2025-12-19 16:08:27 +09:00
kjs 932eb288c6 Merge pull request 'feature/screen-management' (#306) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/306
2025-12-19 16:08:09 +09:00
kjs 09f477172c Merge main into feature/screen-management (menuCopyService 충돌 해결) 2025-12-19 16:07:41 +09:00
kjs 958624012d 복사기능 오류수정 2025-12-19 16:01:57 +09:00
kjs 483dbf8a1f fix: scope_type=menu 채번규칙 삭제 시 check constraint 위반 해결
- scope_type='menu'인 채번규칙: 메뉴 삭제 시 함께 삭제 (파트 포함)
- scope_type!='menu'인 채번규칙: menu_objid만 NULL로 설정 (규칙 보존)
- check_menu_scope_requires_menu_objid 제약조건 준수
2025-12-19 15:52:53 +09:00
SeongHyun Kim 9fb94da493 feat(UniversalFormModal): 섹션별 저장 방식 설정 기능 추가
SectionSaveMode 타입 추가 (공통 저장/개별 저장)
SaveSettingsModal에 섹션별/필드별 저장 방식 설정 UI 추가
saveSingleRow()에 공통 필드 + 품목 병합 저장 로직 구현
buttonActions.ts에 외부 저장 버튼용 병합 저장 처리 추가
2025-12-19 14:53:16 +09:00
hyeonsu f1c775b691 Merge pull request 'reportMng' (#305) from reportMng into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/305
2025-12-19 14:12:32 +09:00
dohyeons 69754a31cb 디지털 3d 뷰어 10초단위 갱신 구현 2025-12-19 14:12:05 +09:00
SeongHyun Kim 9684a83f37 feat(TableSection): 날짜 컬럼 일괄 적용 기능 추가
TableColumnConfig에 batchApply 속성 추가
날짜 타입 컬럼 설정 시 "일괄 적용" 스위치 표시
첫 번째 날짜 입력 시 모든 행에 자동 적용
중복 적용 방지를 위한 batchAppliedFields 플래그 관리
데이터 전체 삭제 시 플래그 리셋
2025-12-19 14:07:35 +09:00
dohyeons 2e7a215066 오른쪽 그리드 크기 조절 2025-12-19 14:00:38 +09:00
kjs 228c497569 fix: 메뉴 복사 로직 개선 - FK 에러 해결 및 성능 최적화
- numbering_rules FK 에러 해결 (menu_objid NULL 설정)
- category_column_mapping FK 에러 해결 (삭제 후 재복사)
- 채번규칙 매핑 보완 로직 추가 (화면에서 참조하는 채번규칙을 이름으로 찾아 매핑)
- 기존 채번규칙/카테고리 매핑의 menu_objid 갱신 로직 추가
- N+1 쿼리 최적화 (배치 조회/삽입으로 변경)
  - 메뉴 삭제: N개 쿼리 → 1개
  - 화면 할당/플로우 수집: N개 쿼리 → 1개
  - 화면 정의 조회: N개 쿼리 → 1개
  - 레이아웃 삽입: N개 쿼리 → 화면당 1개
  - 채번규칙/카테고리 매핑 업데이트: CASE WHEN 배치 처리
- 예상 성능 개선: ~10배
2025-12-19 13:50:13 +09:00
hyeonsu 01422e035b Merge pull request '위젯 컴팩트 모드 제거' (#304) from reportMng into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/304
2025-12-19 13:48:07 +09:00
dohyeons adb21a5308 위젯 컴팩트 모드 제거 2025-12-19 13:47:30 +09:00
SeongHyun Kim 228fd33a2a fix(RepeaterTable): 조회 컬럼 헤더 라벨 개선 및 코드 정리
헤더에 "컬럼명 - 옵션라벨" 형식으로 전체 정보 표시
옵션 변경 시 컬럼 너비 자동 재계산
API 검색 시 정확한 일치 검색(equals) 적용
디버그 로그 제거
설정 UI 라벨 사용자 친화적으로 변경
2025-12-19 13:43:26 +09:00
SeongHyun Kim c86140fad3 feat(UniversalFormModal): 테이블 섹션 컬럼 조회(Lookup) 기능 구현
- LookupConfig, LookupOption, LookupCondition 타입 정의
- sourceType 4가지 유형 지원 (currentRow, sourceTable, sectionField, externalTable)
- TableColumnSettingsModal에 "조회 설정" 탭 추가
- TableSectionSettingsModal에 간단 조회 설정 UI 추가
- fetchExternalValue, fetchExternalLookupValue 함수 구현
- 헤더 드롭다운에서 조회 옵션 선택 기능
2025-12-19 11:48:46 +09:00
hyeonsu 9902b65598 Merge pull request '외부 업체 전용 뷰어 모드 구현' (#303) from reportMng into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/303
2025-12-19 09:42:11 +09:00
dohyeons 981ec27ed7 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into reportMng 2025-12-19 09:41:29 +09:00
kjs 849343ecfd 컴포넌트 통합 계획 2025-12-19 09:27:11 +09:00
kjs 51c788cae8 복사기능 2025-12-19 09:26:44 +09:00
dohyeons 06d2cf7f72 외부 업체 전용 뷰어 모드 구현 2025-12-18 18:14:27 +09:00
SeongHyun Kim fdb9ef9167 feat(RepeaterTable): 컬럼 너비 자동 맞춤 기능 추가
- 균등 분배 / 자동 맞춤 토글 방식으로 변경
- measureTextWidth(): 한글/영문/숫자별 픽셀 계산
- applyAutoFitWidths(): 글자 너비 기준 컬럼 조정
- 계산 규칙 결과 필드를 드롭다운으로 변경
2025-12-18 16:39:10 +09:00
kjs 84efaed1eb 에러 수정 2025-12-18 16:35:55 +09:00
hyeonsu bdb70ce5b7 Merge pull request '3d 전체화면 수정' (#302) from reportMng into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/302
2025-12-18 16:32:04 +09:00
dohyeons 8306d7961c Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into reportMng 2025-12-18 16:31:37 +09:00
dohyeons 61ceab1a7b 외부 업체일 때만 전체화면 되도록 수정 2025-12-18 16:31:25 +09:00
hyeonsu 90d136ca85 Merge pull request '티라유텍 수정사항 적용' (#301) from reportMng into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/301
2025-12-18 16:04:27 +09:00
dohyeons da24db8f37 외부 업체 전용 모드 및 3D 캔버스 전체 화면 기능 구현 2025-12-18 16:03:47 +09:00
dohyeons a617c26721 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into reportMng 2025-12-18 16:02:54 +09:00
kjs 66bd21ee65 엔티티타입 표시오류 수정 2025-12-18 15:24:20 +09:00
SeongHyun Kim 1c6eb2ae61 feat(UniversalFormModal): 테이블 섹션 기능 추가
- FormSectionConfig에 type("fields"|"table") 및 tableConfig 필드 추가
- TableSectionRenderer, TableSectionSettingsModal 신규 컴포넌트 생성
- ItemSelectionModal에 모달 필터 기능 추가 (소스 테이블 distinct 값 조회)
- 설정 패널에서 테이블 섹션 추가/설정 UI 구현
2025-12-18 15:19:59 +09:00
kjs cf8a5a3d93 연쇄관계 자식 라벨표시 2025-12-18 15:16:34 +09:00
dohyeons a24654c867 디지털 트윈 자재 목록 테이블 가독성 개선 2025-12-18 15:11:03 +09:00
dohyeons 79c1a456f0 리스트 위젯 컴팩트 모드 추가 (세로 1칸 대응) 2025-12-18 15:04:55 +09:00
dohyeons ca86c0a10f 위젯 컴팩트 모드 추가 (1x1 사이즈 대응) 2025-12-18 14:42:58 +09:00
kjs 4e987f208a Merge pull request 'feature/screen-management' (#300) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/300
2025-12-18 14:34:54 +09:00
kjs bca6de9811 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-18 14:34:38 +09:00
kjs f03b247db2 카테고리 설정 구현 2025-12-18 14:12:48 +09:00
hyeonsu 176e9cf421 Merge pull request '타입 에러 수정' (#299) from reportMng into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/299
2025-12-18 13:32:59 +09:00
dohyeons 7ca4eea5c1 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into reportMng 2025-12-18 13:32:47 +09:00
dohyeons 41442dccc2 타입 에러 수정 2025-12-18 13:32:22 +09:00
hyeonsu c5f24dc789 Merge pull request 'reportMng' (#298) from reportMng into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/298
2025-12-18 13:28:16 +09:00
dohyeons ac8961160d Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into reportMng 2025-12-18 13:27:25 +09:00
dohyeons 36bac321b8 후판정보 조회 방식 개선 2025-12-18 13:26:11 +09:00
dohyeons 403bd0f8a1 계산 컴포넌트 연산자 로직 개선 2025-12-18 11:41:48 +09:00
kjs 75e5326b3e 메뉴 복사로직 개선 2025-12-18 10:55:26 +09:00
dohyeons 1fd428c016 카드 컴포넌트 추가 및 페이지번호/쿼리 버그 수정 2025-12-18 10:39:57 +09:00
kjs c3f066f88f 테이블 틀고정기능 2025-12-18 10:15:33 +09:00
kjs 061fd45bc8 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-18 09:55:28 +09:00
kjs f1a670ca9a Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-18 09:53:27 +09:00
kjs ff3c51c457 탭 컴포넌트 외부 검색필터 동작 구현 2025-12-18 09:53:26 +09:00
dohyeons 0ed8e686c0 레포트에 페이지번호 컴포넌트 추가 2025-12-18 09:45:07 +09:00
dohyeons 0abe87ae1a 레포트 테이블 수정 방식 수정 2025-12-17 17:59:01 +09:00
dohyeons 6c7807e1d1 리포트 복사 기능 개선 2025-12-17 17:43:21 +09:00
dohyeons c6f0750050 오류 해결 2025-12-17 17:42:38 +09:00
kjs ffd31fc923 Merge pull request '엔티티컬럼 표시설정 수정' (#297) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/297
2025-12-17 17:41:49 +09:00
kjs 7b30f6c7f2 Merge branch 'main' into feature/screen-management 2025-12-17 17:41:43 +09:00
kjs 3589e4a5b9 엔티티컬럼 표시설정 수정 2025-12-17 17:41:29 +09:00
dohyeons 60b4bffdf9 레이아웃 저장/로드 데이터 구조 수정 2025-12-17 17:30:40 +09:00
dohyeons fb4b5b7e26 텍스트 컴포넌트를 textarea로 변경 2025-12-17 17:10:26 +09:00
SeongHyun Kim 8687c88f70 Merge remote-tracking branch 'origin/main' into ksh 2025-12-17 17:07:06 +09:00
SeongHyun Kim 6dcace3135 fix(RepeaterTable): 숫자 필드 포맷팅 로직 개선
- 정수/소수점 자동 구분 처리
- 천 단위 구분자(toLocaleString) 적용
- null/undefined/NaN 예외 처리 추가
2025-12-17 17:04:45 +09:00
dohyeons b7b881ee86 레이블 컴포넌트 제거 2025-12-17 17:02:26 +09:00
dohyeons f47a0c770b 템플릿 팔레트에서 시스템 템플릿 섹션 제거 2025-12-17 16:51:19 +09:00
dohyeons 6f7a76febe AccordionTrigger 내 버튼 중첩 에러 수정 2025-12-17 16:43:29 +09:00
kjs 44f5265105 Merge pull request 'feature/screen-management' (#296) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/296
2025-12-17 16:38:46 +09:00
kjs e50ddd03d3 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-17 16:38:32 +09:00
kjs ae38e0f249 엔티티 타입 에러수정 및 배지 색상없음 오류 수정 2025-12-17 16:38:12 +09:00
SeongHyun Kim 52db6fd43c feat(backend): created_date/updated_date 컬럼 자동 설정 기능 추가
- tableManagementService: insertData()에 created_date 자동 설정
- tableManagementService: updateData()에 updated_date 자동 설정
- dynamicFormService: updateFormRecord()에 updated_date 자동 설정
- 레거시 테이블(sales_order_mng 등) 날짜 컬럼 지원
2025-12-17 16:36:10 +09:00
dohyeons 7acb4981b5 리포트 디자이너 UI 개선 2025-12-17 16:31:58 +09:00
dohyeons 5e0dae0aae Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into reportMng 2025-12-17 16:12:11 +09:00
dohyeons 2e122b0703 Word 변환 WYSIWYG 개선 - 위치/크기/줄바꿈/가로배치 지원 2025-12-17 16:11:52 +09:00
SeongHyun Kim 132cf4cd7d fix(universal-form-modal): 수정 모드에서 옵셔널 필드 그룹 자동 활성화
- 기존 데이터의 triggerField 값이 triggerValueOnAdd와 일치하면 그룹 자동 활성화
- 활성화된 그룹의 필드값도 기존 데이터로 초기화
- 신규 등록 모드에서는 기존대로 비활성화 상태 유지
2025-12-17 15:27:28 +09:00
kjs 0ec6d082d6 Merge pull request 'feature/screen-management' (#295) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/295
2025-12-17 15:00:52 +09:00
SeongHyun Kim 0810debd2b fix(universal-form-modal): 옵셔널 필드 그룹 연동 필드 기본값 설정
- 모달 초기화 시 optionalFieldGroups의 triggerField에 기본값 설정
- triggerValueOnRemove 값을 기본값으로 사용 (비활성화 상태 기본값)
- 수정 모드에서는 기존 데이터 값 유지
2025-12-17 15:00:45 +09:00
kjs 857e46eab6 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-17 15:00:30 +09:00
kjs be916d3db7 탭안에있는 화면 검색필터링 기능 2025-12-17 15:00:15 +09:00
SeongHyun Kim ccbbf46faf feat(universal-form-modal): 옵셔널 필드 그룹 및 카테고리 Select 옵션 기능 추가
- 옵셔널 필드 그룹: 섹션 내 선택적 필드 그룹 지원 (추가/제거, 연동 필드 자동 변경)
- 카테고리 Select: table_column_category_values 테이블 값을 Select 옵션으로 사용
- 전체 카테고리 컬럼 조회 API: GET /api/table-categories/all-columns
- RepeaterFieldGroup 저장 시 공통 필드 자동 병합
2025-12-17 14:30:29 +09:00
kjs 1995c3dca4 엑셀 업로드 기능 개선 2025-12-17 12:01:16 +09:00
kjs 3d287bb883 엔티티타입 연쇄관계관리 설정 추가 2025-12-17 11:48:05 +09:00
SeongHyun Kim 31746e8a0b Merge remote-tracking branch 'origin/main' into ksh 2025-12-16 18:49:03 +09:00
kjs 0832e7b6eb 카드 디스플레이 표시개선 2025-12-16 18:06:15 +09:00
kjs 00afa77d87 Merge pull request 'feature/screen-management' (#294) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/294
2025-12-16 18:02:36 +09:00
kjs d6f40f3cd3 버튼별로 데이터 필터링기능 2025-12-16 18:02:08 +09:00
kjs a73b37f558 좌측 선택데이터에 대한 우측에 데이터표시및 버튼표시 컴포넌트 2025-12-16 16:13:43 +09:00
kjs 3a55ea3b64 분할패널 라벨검색 가능하게 수정 2025-12-16 15:32:43 +09:00
kjs 963e0c2d24 카드 디스플레이
선택안함 옵션 추가
2025-12-16 14:56:31 +09:00
kjs f7e3c1924c 엔티티 즉시저장기능 추가 2025-12-16 14:38:03 +09:00
SeongHyun Kim 342042d761 feat(repeater-table): 행 드래그 앤 드롭 및 컬럼 너비 관리 기능 추가
- @dnd-kit 라이브러리로 행 순서 드래그 앤 드롭 구현
- SortableRow 컴포넌트로 드래그 가능한 테이블 행 구현
- GripVertical 아이콘 드래그 핸들 추가
- 드래그 시 선택된 행 인덱스 자동 재계산
- "균등 분배" 버튼으로 컬럼 너비 컨테이너에 맞게 균등 분배
- 컬럼 헤더 더블클릭으로 데이터 기준 자동 확장/복구 토글
- Input 컴포넌트 min-w-0 w-full 적용으로 컬럼 너비 초과 방지
2025-12-16 13:58:30 +09:00
kjs d8329d31e4 우측화면 데이터 필터링 수정 2025-12-16 11:49:10 +09:00
SeongHyun Kim 56608001ff feat(modal-repeater-table): 체크박스 기반 일괄 삭제 기능 추가
- RepeaterTable: 체크박스 컬럼 추가 (전체 선택/개별 선택 지원)
- RepeaterTable: 선택된 행 시각적 피드백 (bg-blue-50)
- RepeaterTable: 기존 개별 삭제 버튼 컬럼 제거
- ModalRepeaterTableComponent: selectedRows 상태 및 handleBulkDelete 함수 추가
- ModalRepeaterTableComponent: "선택 삭제" 버튼 UI 추가
- RepeatScreenModalConfigPanel: 행 번호 컬럼 선택에서 빈 값 필터링
2025-12-16 11:39:30 +09:00
kjs 4e74c7b5ba 카드 디스플레이 분할패널 설정 2025-12-16 10:46:43 +09:00
kjs b3e6613d66 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-16 10:46:15 +09:00
hyeonsu 27b5f54a7c Merge pull request 'common/feat/dashboard-map' (#293) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/293
2025-12-16 10:30:00 +09:00
dohyeons eb56aec0a7 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-16 10:29:41 +09:00
dohyeons 270b97eec9 RepeaterInput 항목 삭제 개선 및 우측 패널 리렌더링 최적화 2025-12-16 10:29:23 +09:00
hyeonsu 9420b14836 Merge pull request 'common/feat/dashboard-map' (#292) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/292
2025-12-16 10:03:04 +09:00
dohyeons 7688cb8078 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-16 10:02:32 +09:00
dohyeons a2582a28e4 날짜 입력 시 하루 밀리는 타임존 버그 수정 2025-12-16 10:02:16 +09:00
SeongHyun Kim 4cff9e4cec fix(button-actions): 출하계획 모달 데이터 전달 오류 수정
- handleModal: context.selectedRowsData를 selectedData로 복원하여 출하계획 등 모달에서 사용 가능
- handleOpenModalWithData: modalDataStore 데이터를 selectedData/selectedIds로 이벤트에 포함
- ButtonConfigPanel: split-panel-layout2 타입 소스 테이블 감지 지원 추가
- ButtonConfigPanel: column_name/display_name 컬럼 형식 폴백 추가
- ButtonConfigPanel: currentTableName 폴백으로 테이블명 감지 안정성 향상
- ButtonConfigPanel: 필드 매핑 UI를 세로 배치로 변경하여 가독성 개선
2025-12-16 09:13:42 +09:00
SeongHyun Kim cee9903f94 Merge remote-tracking branch 'origin/main' into ksh 2025-12-15 18:43:32 +09:00
SeongHyun Kim f6051e8bbd fix(button-actions): openModalWithData 액션에서 선택된 데이터 전달 누락 수정
- handleOpenModalWithData에서 modalDataStore 데이터를 selectedData/selectedIds로 이벤트에 포함
- RepeatScreenModal에서 groupedData로 사용할 수 있도록 데이터 전달 경로 완성
- ButtonConfigPanel 필드 매핑 UI를 세로 배치로 변경하여 가독성 개선
- split-panel-layout2 컴포넌트 타입 소스 테이블 감지 지원 추가
- currentTableName 폴백 로직 추가로 테이블명 감지 안정성 향상
2025-12-15 18:39:59 +09:00
kjs cb38864ad8 카드 디스플레이 삭제기능 구현 2025-12-15 18:29:18 +09:00
SeongHyun Kim ffb59b4e1b 출하계획 모달 수정 2025-12-15 18:09:55 +09:00
hyeonsu 0ac5402b0b Merge pull request 'common/feat/dashboard-map' (#291) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/291
2025-12-15 18:01:58 +09:00
dohyeons 8cc189da17 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-15 18:01:42 +09:00
dohyeons 8425dece7f 분할 패널 좌측 선택 시 우측 폼 데이터가 갱신되지 않는 문제 수정 2025-12-15 17:47:16 +09:00
SeongHyun Kim 2f66fe1913 Merge origin/main into ksh - resolve conflicts 2025-12-15 17:28:32 +09:00
leeheejin 109380b9e5 이제 디비에 한글로 출발지 목적지 저장되도록 2025-12-15 17:01:04 +09:00
hjlee a495088068 Merge pull request 'lhj' (#290) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/290
2025-12-15 16:55:44 +09:00
leeheejin 54c674f3c9 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2025-12-15 16:54:52 +09:00
leeheejin 7f15861b6e 출발지도착지 디비에서 끌어옴 2025-12-15 16:54:03 +09:00
leeheejin c1be1893f5 지금은 출발지목적지가 달라도 강제로 수정이 가능합니다. 2025-12-15 16:45:26 +09:00
hyeonsu a73f8ae7b3 Merge pull request 'common/feat/dashboard-map' (#289) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/289
2025-12-15 15:59:30 +09:00
dohyeons c52efddae9 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-15 15:59:10 +09:00
dohyeons 93443c98ee 분할 패널 RepeaterFieldGroup 저장 및 DB webType 자동 매핑 구현 2025-12-15 15:40:29 +09:00
kjs 6449eb5ac3 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-15 14:51:54 +09:00
kjs 3c73c20292 화면 복사문제 수정 2025-12-15 14:51:41 +09:00
SeongHyun Kim 16885225a0 feat(edit-modal): 저장 완료 후 제어로직(노드 플로우) 자동 실행 기능 추가
- EditModal에서 INSERT/UPDATE/그룹 저장 완료 후 제어로직 자동 실행
- loadSaveButtonConfig(): 모달 내부 저장 버튼의 제어로직 설정 조회
- findSaveButtonInComponents(): 재귀적으로 저장 버튼 탐색 (conditional-container 내부 포함)
- buttonActions.ts: openEditModal 이벤트에 buttonConfig, buttonContext 전달
- executeAfterSaveControl()을 public으로 변경하여 외부 호출 가능
- 제어로직 실행 오류 시 저장 성공 유지, 경고 토스트만 표시
2025-12-15 14:46:32 +09:00
hjlee 81c3e1b4ba Merge pull request '시간쪽 관련된거' (#288) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/288
2025-12-15 13:46:55 +09:00
leeheejin 93b37e99e6 시간쪽 관련된거 2025-12-15 13:46:42 +09:00
hyeonsu f31fca0115 Merge pull request 'common/feat/dashboard-map' (#287) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/287
2025-12-15 11:28:21 +09:00
dohyeons c2d473bf59 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-15 11:27:44 +09:00
dohyeons 23a1dd6321 분할 패널 테이블에서 셀 클릭 시 행 선택 및 자동 필터링 구현 2025-12-15 11:17:10 +09:00
hyeonsu 4e10449b3f Merge pull request '3D 야드 위젯 새로고침 버튼구현' (#286) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/286
2025-12-15 09:47:58 +09:00
dohyeons d21c4acf0f Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-15 09:47:45 +09:00
dohyeons 95cbd62b1a 3D 야드 위젯 새로고침 버튼구현 2025-12-15 09:46:26 +09:00
SeongHyun Kim f7384cb450 fix(modal-repeater-table): 외부 테이블 조인 시 ID 타입 변환 추가 2025-12-15 09:25:14 +09:00
kjs 665d1b51d8 Merge pull request 'feature/screen-management' (#285) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/285
2025-12-12 18:29:20 +09:00
kjs 036380d267 다중 제어기능 구현 2025-12-12 18:28:58 +09:00
kjs 4777c2bc0a Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-12 17:09:41 +09:00
kjs b755f8f017 분할패널 소스컬럼 추론 2025-12-12 17:08:36 +09:00
hjlee e8bc770439 Merge pull request 'lhj' (#284) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/284
2025-12-12 15:47:15 +09:00
leeheejin 8f6af5018c Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2025-12-12 15:46:43 +09:00
leeheejin 76f6bd7f27 설정들 고친거 2025-12-12 15:45:57 +09:00
kjs 722718b7ed 설비 수정모달 데이터 안넘어오는 현상 수정 2025-12-12 14:37:24 +09:00
SeongHyun Kim 7ad70462d5 Merge branch 'ksh' 2025-12-12 14:14:58 +09:00
SeongHyun Kim 51099ba858 fix(modal-repeater-table): 외부 테이블 조인 시 ID 타입 변환 추가
문제:
- 외부 테이블 조인 시 ID 값이 문자열로 전달되어 백엔드에서 ILIKE 검색 수행
- 문자열 '189'로 검색하면 '189', '1890', '18900' 등 모두 매칭되는 문제
- 발주 등록 화면에서 품목 참조 데이터 조회 실패

해결:
- fetchReferenceValue 함수: 조인 조건 값 타입 변환 추가
- resolveDynamicValue 함수 (단순 테이블 조회): 조인 조건 값 타입 변환 추가
- resolveDynamicValue 함수 (복합 조인): 조인 조건 값 타입 변환 추가

변환 로직:
- targetField가 '_id'로 끝나거나 'id'인 경우 Number()로 변환
- NaN 체크로 변환 불가능한 값은 원본 유지
- 백엔드에서 숫자는 = 비교, 문자열은 ILIKE 검색 수행하므로 정확한 매칭 필요

영향 범위:
- modal-repeater-table 컴포넌트를 사용하는 모든 화면
- 발주 등록, 수주 등록 등 품목 참조 테이블 조회
2025-12-12 14:12:33 +09:00
SeongHyun Kim 11215e3316 chore: 미사용 수주 등록 모듈(orderController) 삭제
- 백엔드: orderController.ts, orderRoutes.ts 삭제
- 프론트엔드: components/order/, order-registration-modal/ 삭제
- app.ts, index.ts, getComponentConfigPanel.tsx에서 참조 제거
- 현재 sales_order_mng 기반 수주 시스템 사용으로 구 모듈 불필요
2025-12-12 14:02:17 +09:00
kjs 309d4be31d Merge pull request 'feature/screen-management' (#283) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/283
2025-12-12 13:50:49 +09:00
kjs add98673bb Merge branch 'main' into feature/screen-management 2025-12-12 13:50:43 +09:00
kjs 3a6af2fb71 분할패널 조인문제 수정 2025-12-12 13:50:33 +09:00
SeongHyun Kim c85841b59f fix(repeat-screen-modal): 외부 테이블 조인 시 ID 타입 변환 추가
- 조인 키가 '_id' 또는 'id'인 경우 문자열을 숫자로 변환
- 백엔드 ILIKE 검색 방지로 정확한 ID 매칭 보장
- API 호출 파라미터 로깅 추가 (디버깅용)
2025-12-12 13:46:20 +09:00
hjlee ac3de6ab07 Merge pull request 'lhj' (#282) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/282
2025-12-12 13:45:29 +09:00
SeongHyun Kim 1680163c61 fix: ModalRepeaterTable 빈 행 자동 표시 문제 해결
- 신규 등록 모달 오픈 시 빈 객체 필터링 기능 추가
- isEmptyRow 함수로 안전한 빈 객체 판단 (id 필드 체크)
- useState 초기화 및 useEffect 동기화에 필터링 적용
- 수정 모달의 실제 데이터는 id 필드로 보호
2025-12-12 11:10:51 +09:00
SeongHyun Kim a9135165d9 fix: UniversalFormModal 채번 규칙 자동 생성 기능 개선
- 모달 재오픈 시 동일 번호 유지 (previewCode 사용)
- 저장 시 정상적인 순번 증가 (allocateCode에서 nextSequence 사용)
- refreshKey를 React key로 전달하여 컴포넌트 강제 리마운트
- ruleId를 부모 컴포넌트까지 전달하여 buttonActions에서 감지
- 미리보기와 저장 번호 일치 (currentSequence + 1 통일)
2025-12-12 10:55:09 +09:00
kjs 0ee49b77ae 설비 품목 하나만 추가되는 오류 수정 2025-12-12 10:44:59 +09:00
SeongHyun Kim 5ed80df2d4 Merge branch 'ksh' 2025-12-12 09:15:26 +09:00
SeongHyun Kim ab8b5a2c91 fix: UniversalFormModal 채번규칙 중복 호출 문제 해결
- generateNumberingValues 중복 호출 방지 (ref 플래그 추가)
- generateOnOpen 시 allocateCode 직접 호출로 변경
- config 변경 시 initializeForm 재호출 비활성화
- cleanup 함수에서 플래그 초기화 추가
- 저장 시점 채번 로직 간소화 (generateOnSave만 처리)
2025-12-11 19:14:55 +09:00
kjs e8af7ae4c6 Merge pull request '분할패널 버튼 이동 가능하게 수정' (#281) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/281
2025-12-11 18:40:57 +09:00
kjs 016b8f707b 분할패널 버튼 이동 가능하게 수정 2025-12-11 18:40:39 +09:00
SeongHyun Kim 038c5a0973 fix(numbering-rule): 채번 미리보기 순번 수정 및 저장 시 재할당 로직 추가
- 미리보기 시 currentSequence + 1로 다음 순번 표시
- UniversalFormModal에서 미리보기/실제할당 분리
- _needsAllocation 플래그로 저장 시 재할당 여부 판단
- RepeatScreenModal 외부 데이터 소스 조인/필터 설정 UI 추가
2025-12-11 18:26:33 +09:00
hyeonsu 88024b4e60 Merge pull request 'fix: tiptap 버전 충돌 해결 및 legacy-peer-deps 적용' (#280) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/280
2025-12-11 16:32:33 +09:00
dohyeons fcc709684b Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-11 16:32:15 +09:00
dohyeons b208b0be34 fix: tiptap 버전 충돌 해결 및 legacy-peer-deps 적용 2025-12-11 16:31:47 +09:00
leeheejin c2a6dbea3b Merge branch 'main' into lhj - resolve TableListComponent conflict 2025-12-11 16:28:46 +09:00
leeheejin 563acb7c00 봄 에러 뜨던거 고침2 2025-12-11 16:09:58 +09:00
leeheejin 30361e0f45 돔 에러 뜨던거 고침 2025-12-11 16:07:16 +09:00
leeheejin 84bd1ce154 첨부파일 이름으로 나오게 함 2025-12-11 15:50:28 +09:00
leeheejin a489e2c155 첨부파일 기능 완료 2025-12-11 15:46:05 +09:00
SeongHyun Kim 6a676dcf5c refactor(universal-form-modal): ConfigPanel 모달 분리 및 설정 패널 오버플로우 수정
- UniversalFormModalConfigPanel을 3개 모달로 분리 (2300줄 → 300줄)
  - FieldDetailSettingsModal: 필드 상세 설정
  - SaveSettingsModal: 저장 설정
  - SectionLayoutModal: 섹션 레이아웃 설정
- FloatingPanel, DetailSettingsPanel 가로 스크롤 오버플로우 수정
- SelectOptionConfig에 saveColumn 필드 추가 (저장 값 별도 지정)
2025-12-11 15:29:37 +09:00
dohyeons 308c78b067 배포 오류 해결 2025-12-11 15:29:25 +09:00
hyeonsu 3f1ecfab15 Merge pull request '외부 DB 연결 끊김 오류 해결' (#279) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/279
2025-12-11 15:26:16 +09:00
dohyeons ab9ddaa190 외부 DB 연결 끊김 오류 해결 2025-12-11 15:25:48 +09:00
dohyeons b03132595c 패키지 다운그레이드 2025-12-11 14:38:57 +09:00
dohyeons e67b5f76a8 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node 2025-12-11 14:34:48 +09:00
dohyeons 1d97bcaa9f 배포 오류 해결 2025-12-11 14:34:42 +09:00
kjs d3b4c4c42e Merge pull request 'feature/screen-management' (#278) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/278
2025-12-11 14:32:34 +09:00
kjs f2b0ac8fd5 Merge branch 'main' into feature/screen-management 2025-12-11 14:32:27 +09:00
kjs a4cf11264d 테이블리스트컴포넌트 합산기능 2025-12-11 14:32:15 +09:00
kjs 215242b676 검색필터 분할패널 데이터 합산기능 추가 2025-12-11 14:25:28 +09:00
SeongHyun Kim 190a677067 feat(modal-repeater-table): 컬럼 너비 리사이즈 기능 및 엑셀 스타일 UI 개선
- 컬럼 헤더 드래그로 너비 조정 기능 추가 (최소 60px)
- 헤더 더블클릭으로 기본 너비 복원 기능 추가
- 엑셀 스타일 테두리 및 색상 적용 (border-b border-r)
- 테이블 최대 높이 240px → 400px 확장
- 입력 필드 높이 및 포커스 스타일 개선
2025-12-11 14:05:34 +09:00
hjlee 4247c3bb70 Merge pull request 'lhj' (#277) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/277
2025-12-11 13:53:52 +09:00
leeheejin 99fd8336a5 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2025-12-11 13:48:57 +09:00
leeheejin c486a31787 파일업로드 로직 중간저장(다듬기하면됨) 2025-12-11 13:48:34 +09:00
SeongHyun Kim 9463d8d0b6 수주관리 수정 모달 수정 2025-12-11 13:25:13 +09:00
hyeonsu 1bb6448b1f Merge pull request 'common/feat/dashboard-map' (#276) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/276
2025-12-11 13:15:56 +09:00
dohyeons 011f0556d2 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-11 13:15:19 +09:00
dohyeons f6e0e02ddf 회사 관리자 메뉴 권한 필터링 적용 2025-12-11 13:15:09 +09:00
SeongHyun Kim 84095ace3b feat(button-actions): 저장 후 노드 플로우 실행 기능 추가 및 RepeatScreenModal props 수신 개선
- dataflowConfig.flowConfig 설정 시 저장 완료 후 노드 플로우 자동 실행
- executeNodeFlow API 동적 import로 번들 최적화
- RepeatScreenModal에서 _groupedData props 수신 지원 추가
- tiptap 라이브러리 버전 업그레이드 (2.11.5 → 2.27.1)
2025-12-11 13:05:12 +09:00
kjs fc5ffb03b2 엔티티 조인컬럼 표시문제 수정 2025-12-11 12:01:00 +09:00
SeongHyun Kim 0e60f11084 Merge remote-tracking branch 'origin/main' into ksh 2025-12-11 11:47:43 +09:00
kjs 2327dbe35c Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-11 11:41:39 +09:00
kjs b9ee860e71 분할 패널 수정,삭제버튼 on,off기능 2025-12-11 11:41:38 +09:00
kjs fcdaa68ddc Merge pull request '소스필드 못찾는 버그 수정' (#275) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/275
2025-12-11 11:37:58 +09:00
kjs 91f9bb9d12 Merge branch 'main' into feature/screen-management 2025-12-11 11:37:52 +09:00
kjs 2b747a1030 소스필드 못찾는 버그 수정 2025-12-11 11:37:40 +09:00
hyeonsu 6735142db4 Merge pull request '모바일 환경 세션 타임아웃 연장 (30분 → 24시간)' (#274) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/274
2025-12-11 11:18:47 +09:00
dohyeons c4e81dd740 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-11 11:18:30 +09:00
dohyeons 7725cd1e87 모바일 환경 세션 타임아웃 연장 (30분 → 24시간) 2025-12-11 11:17:52 +09:00
hyeonsu d729d299a9 Merge pull request 'common/feat/dashboard-map' (#273) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/273
2025-12-11 10:50:02 +09:00
dohyeons d6c5b3418d Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-11 10:49:12 +09:00
dohyeons bccb8a6330 리스트 위젯 REST API 기능 개선 2025-12-11 10:48:48 +09:00
kjs 288e553221 Merge pull request 'feature/screen-management' (#272) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/272
2025-12-11 10:44:16 +09:00
kjs 5bbbd37553 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-11 10:43:09 +09:00
kjs f272f0c4c7 제어관리 회사코드 저장 안되는 문제 수정 2025-12-11 10:41:28 +09:00
SeongHyun Kim 1a68ae792e fix(repeat-screen-modal): 외부 테이블 행 삭제를 즉시 DELETE API 호출 방식으로 변경
- 기존 소프트 삭제(_isDeleted 플래그) 방식에서 즉시 삭제로 변경
- DB에 저장된 기존 행: DELETE API 즉시 호출 후 UI에서 제거
- 새로 추가된 행: UI에서만 제거 (DB에 없음)
- _originalData.id 존재 여부로 DB 데이터 판단
- 삭제 후 집계 재계산 정상 작동
2025-12-11 09:30:37 +09:00
SeongHyun Kim 512e1e30d1 feat(repeat-screen-modal): 연동 저장, 자동 채번, SUM_EXT 참조 제한 기능 추가
- SyncSaveConfig: 모달 저장 시 다른 테이블에 집계 값 동기화 기능
- RowNumberingConfig: 행 추가 시 채번 규칙 적용하여 자동 번호 생성
- externalTableRefs: SUM_EXT 함수가 참조할 외부 테이블 제한 기능
- triggerRepeatScreenModalSave: 외부에서 저장 트리거 가능한 이벤트 리스너
- TableColumnConfig.hidden: 테이블 컬럼 숨김 기능 (데이터는 유지, 화면만 숨김)
- beforeFormSave: FK 자동 채우기 및 _isNew 행 포함 로직 개선
2025-12-11 09:24:47 +09:00
SeongHyun Kim ae6f022f88 feat(repeat-screen-modal): 복수 외부 테이블 집계 지원 및 집계 설정 모달 UI 추가
- 여러 외부 테이블 데이터를 합산하여 집계 계산 지원
- 집계 설정 전용 모달(AggregationSettingsModal) 추가
- AggregationConfig에 hidden 속성 추가 (연산에만 사용, 표시 제외)
- 채번 규칙 API 에러 처리 개선 (조용히 무시, 로그 최소화)
2025-12-11 09:17:57 +09:00
leeheejin d09c8e0787 파일업로드 수정 2025-12-10 18:38:16 +09:00
kjs 088596480f 수식 노드 구현 2025-12-10 18:28:27 +09:00
leeheejin fa6c00b6be 모달 잘리는거 해결 2025-12-10 17:41:41 +09:00
hjlee e84764dc2b Merge pull request 'lhj' (#271) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/271
2025-12-10 16:49:21 +09:00
leeheejin bc10f2101a Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2025-12-10 16:48:19 +09:00
leeheejin 65c1855eba 기본정보 눌렀을때 뜨는 오류해결 2025-12-10 16:47:48 +09:00
hyeonsu d0801e2ccc Merge pull request 'common/feat/dashboard-map' (#270) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/270
2025-12-10 16:25:51 +09:00
kjs 3188bc0513 입고테이블 생성날짜 저장에러ㅏ 수정 2025-12-10 16:06:47 +09:00
kjs 08575c296e 연쇄 통합관리 2025-12-10 15:59:04 +09:00
dohyeons 48300146e6 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-10 15:54:09 +09:00
hjlee 5cd3bc52e3 Merge pull request 'lhj' (#269) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/269
2025-12-10 15:53:33 +09:00
leeheejin 6707e2afd2 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2025-12-10 15:53:11 +09:00
dohyeons 90db4756e8 3D 야드 자재 개수 표시 버그 수정 및 빈 Location 표시 추가 2025-12-10 15:41:43 +09:00
dohyeons e6b8212d39 외부 커넥션 모달에 스크롤 생성 2025-12-10 15:40:33 +09:00
leeheejin d1c9aeca18 리스트위젯 조금 더 개선된 버전 2025-12-10 15:29:23 +09:00
leeheejin f75c3e43ed 리스트 위젯 업그레이드 2025-12-10 15:15:06 +09:00
dohyeons d7e96327a7 변경 이력 모달의 작업 컬럼 스타일을 수정 2025-12-10 15:11:46 +09:00
hyeonsu 9998045013 Merge pull request '플로우 위젯 체크박스 선택 버그 수정 - 인덱스 기반에서 Primary Key 기반으로 변경' (#268) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/268
2025-12-10 14:39:38 +09:00
dohyeons c7ae04859d 플로우 위젯 체크박스 선택 버그 수정 - 인덱스 기반에서 Primary Key 기반으로 변경 2025-12-10 14:28:11 +09:00
kjs c71b958a05 연쇄관계 관리 2025-12-10 13:53:44 +09:00
leeheejin c64c94c07b 최근이동한 내역들 2025-12-10 13:48:57 +09:00
hjlee 28ca4f2088 Merge pull request '중간저장' (#267) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/267
2025-12-10 10:30:41 +09:00
leeheejin 3608d9f9c3 중간저장 2025-12-10 10:27:54 +09:00
hjlee 990f667481 Merge pull request '테이블 툴바 설정 제대로 됩니당' (#266) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/266
2025-12-10 09:17:32 +09:00
leeheejin dde65a2d1e 테이블 툴바 설정 제대로 됩니당 2025-12-10 09:17:11 +09:00
hjlee d11ffb1b00 Merge pull request '툴바 선택' (#265) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/265
2025-12-09 18:26:55 +09:00
leeheejin 93ec294be3 툴바 선택 2025-12-09 18:26:36 +09:00
kjs ba817980f0 투명색 설정 가능하게 구현 2025-12-09 17:08:10 +09:00
hjlee 74da2cd97f Merge pull request '데이터 증식하는 문제 해결' (#264) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/264
2025-12-09 17:04:12 +09:00
leeheejin 36a7529da2 데이터 증식하는 문제 해결 2025-12-09 16:54:47 +09:00
hyeonsu 893ae06f19 Merge pull request 'common/feat/dashboard-map' (#263) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/263
2025-12-09 16:43:21 +09:00
dohyeons 8b200ba9f3 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-09 16:42:37 +09:00
dohyeons 94b371ca0f 공차등록/운행알림 기능 개선 - vehicles 테이블 출발지/도착지 저장 및 운행종료 시 초기화 2025-12-09 16:38:47 +09:00
SeongHyun Kim 5e97a3a5e9 fix: 화면 복사 코드 생성 로직 개선 및 UniversalFormModal beforeFormSave 이벤트 연동
- screenManagementService: PostgreSQL regexp_replace로 정확한 최대 번호 조회
- CopyScreenModal: linkedScreens 의존성 추가로 모달 코드 생성 보장
- UniversalFormModal: beforeFormSave 이벤트 리스너로 ButtonPrimary 연동
- 설정된 필드만 병합하여 의도치 않은 덮어쓰기 방지
2025-12-09 16:11:04 +09:00
hjlee 0f8817835e Merge pull request '테이블리스트로 세금계산서 만들기' (#262) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/262
2025-12-09 15:13:16 +09:00
leeheejin a8cbc289f6 테이블리스트로 세금계산서 만들기 2025-12-09 15:12:59 +09:00
SeongHyun Kim d550959cb7 feat(modal-repeater-table): 동적 데이터 소스 전환 기능 및 UniversalFormModal 저장 버튼 옵션 추가
- ModalRepeaterTable: 컬럼 헤더 클릭으로 데이터 소스 동적 전환
- 단순 조인, 복합 조인(다중 테이블), 전용 API 호출 지원
- DynamicDataSourceConfig, MultiTableJoinStep 타입 추가
- 설정 패널에 동적 데이터 소스 설정 모달 추가
- UniversalFormModal: showSaveButton 옵션 추가
2025-12-09 14:55:49 +09:00
kjs 1506389757 메일발송기능 데이터 모달로 전달 2025-12-09 14:17:45 +09:00
kjs ece7f21bd3 메일 본문 내용 사용자 경험 개선 2025-12-09 13:50:17 +09:00
kjs 1ee1287b8a 메일 수신자 컴포넌트 구현 2025-12-09 13:29:20 +09:00
hyeonsu a2e99b30e6 Merge pull request '작동하지 않는 버튼 비활성화' (#261) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/261
2025-12-09 12:16:47 +09:00
dohyeons 2479e3a0c4 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-09 12:16:39 +09:00
dohyeons 167c3cd26b 작동하지 않는 버튼 비활성화 2025-12-09 12:14:32 +09:00
kjs bb98e9319f 외부호출 노드들 2025-12-09 12:13:30 +09:00
hyeonsu 6de40bea0c Merge pull request 'UTC DB 환경인 실 서비스에서의 9시간 지연 표시 문제 해결' (#260) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/260
2025-12-09 12:05:33 +09:00
dohyeons 612b46236f UTC DB 환경인 실 서비스에서의 9시간 지연 표시 문제 해결 2025-12-09 12:05:12 +09:00
hyeonsu d7e03d6b83 Merge pull request 'flowExecutionService 트랜잭션 처리 개선 및 데이터 변경 추적 로직 수정' (#259) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/259
2025-12-09 11:16:36 +09:00
dohyeons 3cf99dbad9 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-09 11:16:23 +09:00
dohyeons 0aaab45329 flowExecutionService 트랜잭션 처리 개선 및 데이터 변경 추적 로직 수정 2025-12-09 11:15:18 +09:00
kjs cf73ce6ebb Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-09 10:47:15 +09:00
kjs 987120f13b 참조조회 노드 제거 2025-12-09 10:47:15 +09:00
hyeonsu aa78c0c0cb Merge pull request 'common/feat/dashboard-map' (#258) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/258
2025-12-09 10:38:05 +09:00
dohyeons 8d07458c94 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-09 10:36:49 +09:00
hjlee 84f47a021b Merge pull request '지역 필터링 기능 추가' (#257) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/257
2025-12-09 10:34:24 +09:00
leeheejin 469c8b2e57 지역 필터링 기능 추가 2025-12-09 10:18:07 +09:00
SeongHyun Kim 7ac6bbc2c6 Merge remote-tracking branch 'origin/main' into ksh 2025-12-09 09:24:33 +09:00
SeongHyun Kim fa59235cd2 fix(split-panel-layout2): 좌측 패널 항목 선택 상태 비교 로직 개선
- idColumn 자동 감지 로직 추가 (id > dept_code > code 순 폴백)
- isSelected 비교 시 객체 동일성 및 undefined 체크 추가
- hierarchyConfig.idColumn 미설정 시에도 정상 동작
2025-12-09 09:22:10 +09:00
SeongHyun Kim d908de7f66 fix(numbering-rule): 채번규칙 저장 시 allocateNumberingCode로 실제 순번 할당
- generateNumberingCode를 allocateNumberingCode로 변경 (순번 실제 증가)

- saveSingleRow/saveMultipleRows/saveWithMultiTable 모두 적용

- NumberingRuleCard: 파트 타입 변경 시 defaultAutoConfig 적용

- NumberingRuleDesigner: 저장 시 partsWithDefaults로 기본값 병합

- sequenceLength/numberLength 기본값 4에서 3으로 변경

- 불필요한 console.log 제거
2025-12-08 19:10:07 +09:00
kjs 531ba3ffdb Merge pull request '빌드 에러 수정' (#256) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/256
2025-12-08 18:26:56 +09:00
kjs fb4c9574d3 Merge branch 'main' into feature/screen-management 2025-12-08 18:26:49 +09:00
kjs e53515481b 빌드 에러 수정 2025-12-08 18:26:38 +09:00
SeongHyun Kim b15b6e21ea fix(UniversalFormModal): 반복 섹션 linkedFieldGroup 매핑 및 서브 테이블 저장 로직 개선
- renderFieldWithColumns()에 repeatContext 파라미터 추가

- linkedFieldGroup 선택 시 repeatContext 유무에 따라 formData/repeatSections 분기 저장

- multiTableSave: UPSERT 대신 SELECT-UPDATE/INSERT 명시적 분기로 변경

- ON CONFLICT 조건 불일치 에러 방지

- 서브 테이블 저장 상세 로그 추가
2025-12-08 18:23:28 +09:00
hjlee 37fb9a13f8 Merge pull request 'lhj' (#255) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/255
2025-12-08 18:21:51 +09:00
leeheejin a20712d48e Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2025-12-08 18:17:27 +09:00
leeheejin 5b456765ad 대시보드 통계카드 위젯에 소수점 자릿수 표시 할 수 있는 기능 추가 2025-12-08 18:16:59 +09:00
kjs 33e7767f75 Merge pull request 'feature/screen-management' (#254) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/254
2025-12-08 17:59:31 +09:00
kjs 2b055757e2 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-08 17:59:09 +09:00
kjs 92a7e0eb3a 렉구조 중복등록 방지 2025-12-08 17:56:56 +09:00
SeongHyun Kim a278ceca3f feat(universal-form-modal): 범용 다중 테이블 저장 기능 추가 2025-12-08 17:54:11 +09:00
kjs 5609e6353f 창고 렉 구조 등록 컴포넌트 중복 방지기능 추가 2025-12-08 17:13:14 +09:00
dohyeons f0f6c42b3c flow-widget 인라인 편집 시 changed_by에 사용자 ID 기록되도록 수정 2025-12-08 16:49:28 +09:00
dohyeons ad5c7f643c Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-08 16:37:13 +09:00
hjlee 0df53f46b3 Merge pull request 'lhj' (#253) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/253
2025-12-08 16:19:19 +09:00
leeheejin ed1626d391 세금계산서 업그레이드 2025-12-08 16:18:44 +09:00
dohyeons 11a99a5c2e flow-widgdt 인라인 편집 및 검색 하이라이트 기능 추가 2025-12-08 16:06:43 +09:00
leeheejin 0ffec7f443 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2025-12-08 16:02:19 +09:00
leeheejin ab1308efe8 세금계산서 발행 완료 2025-12-08 16:01:59 +09:00
kjs ec65ad6b9e 데이터전달 모달열기 액션에 컬럼 매핑기능 추가 2025-12-08 15:50:58 +09:00
SeongHyun Kim 3dc67dd60a Merge remote-tracking branch 'origin/main' into ksh 2025-12-08 15:35:38 +09:00
SeongHyun Kim 61c1f10495 feat(ModalRepeaterTable): 항목 검색 모달 컬럼 라벨 설정 기능 추가
- sourceColumnLabels 타입 정의 (Record<string, string>)

- ConfigPanel에 소스 컬럼별 표시 라벨 입력 UI 추가

- columnLabels 생성 시 sourceColumnLabels 우선 적용

- 컬럼 삭제 시 해당 라벨도 함께 삭제

- 빈 상태 안내 메시지 추가
2025-12-08 15:34:19 +09:00
SeongHyun Kim 892278853c feat(UniversalFormModal): 전용 API 저장 기능 및 사원+부서 통합 저장 API 구현
- CustomApiSaveConfig 타입 정의 (apiType, mainDeptFields, subDeptFields)

- saveWithCustomApi() 함수 추가로 테이블 직접 저장 대신 전용 API 호출

- adminController에 saveUserWithDept(), getUserWithDept() API 추가

- user_info + user_dept 트랜잭션 저장, 메인 부서 변경 시 자동 겸직 전환

- ConfigPanel에 전용 API 저장 설정 UI 추가

- SplitPanelLayout2: getColumnValue()로 조인 테이블 컬럼 값 추출 개선

- 검색 컬럼 선택 시 표시 컬럼 기반으로 변경
2025-12-08 15:31:40 +09:00
SeongHyun Kim a5055cae15 feat(SplitPanelLayout2): 추가 조인 테이블 기능 구현
- JoinTableConfig 타입 정의 (joinTable, joinType, mainColumn, joinColumn, selectColumns)

- RightPanelConfig.joinTables 배열 추가로 다중 조인 지원

- loadJoinTableData(), mergeJoinData() 함수로 클라이언트 사이드 조인 처리

- JoinTableItem 컴포넌트로 조인 테이블 설정 UI 제공

- 표시 컬럼에 sourceTable 추가로 테이블별 컬럼 구분

- 메인+조인 테이블 컬럼 통합 로드 기능
2025-12-08 15:27:28 +09:00
SeongHyun Kim de1fe9865a refactor(UniversalFormModal): 다중 컬럼 저장 기능을 필드 레벨로 이동
- 섹션 레벨 linkedFieldGroups 제거, 필드 레벨 linkedFieldGroup으로 변경

- FormFieldConfig에 linkedFieldGroup 속성 추가 (enabled, sourceTable, displayColumn, displayFormat, mappings)

- select 필드 렌더링에서 linkedFieldGroup 활성화 시 다중 컬럼 저장 처리

- API 응답 파싱 개선 (responseData.data 구조 지원)

- 저장 실패 시 상세 에러 메시지 표시

- ConfigPanel에 다중 컬럼 저장 설정 UI 및 HelpText 추가
2025-12-08 15:16:45 +09:00
kjs 274078ef2c Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-08 15:16:07 +09:00
kjs e05af3c6f9 렉 구조등록 컴포넌트 2025-12-08 15:15:44 +09:00
SeongHyun Kim 0c57609ee9 feat(UniversalFormModal): 연동 필드 그룹 기능 추가
- LinkedFieldGroup, LinkedFieldMapping 타입 정의

- 소스 테이블 데이터 캐싱 및 드롭다운 렌더링

- 선택 시 여러 컬럼에 자동 값 매핑 처리

- 설정 패널에 연동 필드 그룹 관리 UI 추가

- 일반 섹션/반복 섹션 모두 지원
2025-12-08 15:13:33 +09:00
leeheejin f04a3e3505 세금계산서 하기 전에 저장 2025-12-08 14:34:18 +09:00
kjs 09d2d7573d 화면 같이 줄어들게 수정 2025-12-08 11:44:07 +09:00
dohyeons 2cc0a7b309 배치 스케줄러 타임존을 Asia/Seoul로 설정 2025-12-08 10:34:37 +09:00
hjlee 7f296afc17 Merge pull request 'lhj' (#252) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/252
2025-12-08 10:24:23 +09:00
leeheejin 8ec5c987de restapi 도 경로보기 가능, 출발지목적지 동시에 같은거 못하게, 자물쇠걸면 컬럼 수정 못함 tablelistcomponent 2025-12-08 10:23:54 +09:00
leeheejin 7a596cad3d Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2025-12-08 09:24:33 +09:00
hyeonsu c98257a794 Merge pull request '지도 위젯 REST API Request Body 전달 오류 수정' (#251) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/251
2025-12-05 18:33:51 +09:00
dohyeons 4c4e7965d7 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-05 18:33:38 +09:00
leeheejin c39794d1a7 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2025-12-05 18:30:44 +09:00
dohyeons 46ef858c1d 지도 위젯 REST API Request Body 전달 오류 수정 2025-12-05 18:29:32 +09:00
kjs cbe5cb4607 토큰 자동 갱신 기능 추가 및 에러 처리 개선 2025-12-05 17:46:22 +09:00
leeheejin 65227c5e03 자물쇠 누르면 컬럼 값 변경 안됩니다. 2025-12-05 17:42:35 +09:00
kjs 47552bc35c 집계함수 제어 수정 2025-12-05 17:28:44 +09:00
leeheejin 417d77729d 일단 월요일에 상의해야해서 여기에다만 커밋 2025-12-05 16:44:58 +09:00
kjs e713f55442 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-05 15:22:28 +09:00
kjs 96321f502f 제어 집계함수 노드 추가 2025-12-05 15:18:55 +09:00
SeongHyun Kim 7a185ca1ed Merge ksh branch up to commit 5d3b3ea7 (날짜 필드 ISO 형식 변환 수정) 2025-12-05 15:18:04 +09:00
kjs 1c329b5e0c 화면 분할 패널 자동으로 데이터 넘기는 기능 설정 가능하게 변경 2025-12-05 14:08:07 +09:00
hyeonsu a866647506 Merge pull request 'common/feat/dashboard-map' (#250) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/250
2025-12-05 14:02:19 +09:00
dohyeons 1a77a5b28a Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-05 14:01:17 +09:00
dohyeons 8e3452a04f 배포 DB 연결 정보 변경 2025-12-05 14:00:49 +09:00
hjlee 6da1590430 Merge pull request '3d 야드 수정' (#249) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/249
2025-12-05 13:46:02 +09:00
leeheejin a1daa63dcc 3d 야드 수정 2025-12-05 13:45:44 +09:00
hjlee 8781e9c6c3 Merge pull request 'lhj' (#248) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/248
2025-12-05 12:47:19 +09:00
leeheejin d2bd623d9a 공차/운행 ~ 운행알림까지 걸린 거리, 시간 기록하기 2025-12-05 11:59:11 +09:00
kjs 662956edd4 엔티티 카테고리 타입 컬럼 배지 표시기능 2025-12-05 11:04:42 +09:00
leeheejin ccf8bd3284 버튼활성화비활성화 2025-12-05 11:03:15 +09:00
kjs 9e956999c5 모달 크기 고정 2025-12-05 10:46:10 +09:00
dohyeons 7c06b98f86 배치 수정 페이지 버그 수정 및 멀티테넌시 보안 강화 2025-12-05 10:36:52 +09:00
dohyeons b6a7b4a93b 배치 수정 페이지 저장 및 API 미리보기 버그 수정 2025-12-05 10:16:23 +09:00
SeongHyun Kim 5d3b3ea76e fix(modal-repeater-table): 날짜 필드 ISO 형식 변환으로 표시 오류 수정
- RepeaterTable에서 DB 조회된 ISO 형식 날짜를 yyyy-mm-dd로 변환
- formatDateValue 함수 추가: ISO 문자열, Date 객체, 기존 형식 모두 처리
- 수주일(order_date), 납기일(item_due_date) 등 날짜 필드 정상 표시
2025-12-05 10:13:59 +09:00
dohyeons 58ca340699 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-04 19:48:34 +09:00
dohyeons 16d30632a0 배치 수정 페이지 레이아웃 및 에러 개선 2025-12-04 19:48:10 +09:00
SeongHyun Kim 5c12b9fa83 Merge origin/main into ksh - resolve split-panel-layout2 conflicts 2025-12-04 19:19:58 +09:00
SeongHyun Kim c1400081c6 fix(modal-repeater-table): 품목 추가 시 UI 즉시 반영되지 않는 버그 수정
- value 상수를 localValue useState로 변경하여 내부 상태 관리
- useEffect로 외부 값(formData, propValue) 변경 시 동기화
- handleChange에서 setLocalValue 호출하여 즉각적인 UI 업데이트
- RepeaterTable, ItemSelectionModal 등 모든 참조를 localValue로 변경
2025-12-04 19:17:32 +09:00
SeongHyun Kim 0e4ecef336 feat(universal-form-modal): 필수 필드 검증 및 섹션 레이아웃 열 수 설정 기능 추가
- validateRequiredFields 함수 추가로 필수 필드 미입력 시 저장 차단
- 섹션별 열 수 설정 (1열/2열/3열/4열) 및 gridSpan 자동 계산
- 버튼 이벤트 버블링 방지 (type=button, preventDefault, stopPropagation)
- onChange 콜백 렌더링 사이클 분리 (setTimeout)
- 다중 행 저장 시 빈 객체 건너뛰기 로직 추가
2025-12-04 19:17:28 +09:00
SeongHyun Kim 6c751eb489 feat(universal-form-modal): 범용 폼 모달 컴포넌트 신규 개발
- 섹션 기반 폼 레이아웃 지원 (접힘/펼침, 그리드 컬럼)
- 반복 섹션 지원 (겸직 등 동일 필드 그룹 여러 개 추가)
- 채번규칙 연동 (모달 열릴 때 또는 저장 시점 자동 생성)
- 다중 행 저장 지원 (공통 필드 + 개별 필드 조합)
- Select 옵션 동적 로드 (정적/테이블/공통코드)
- 스크린 디자이너 설정 패널 구현
2025-12-04 19:13:58 +09:00
kjs a38650692c Merge pull request 'feature/screen-management' (#247) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/247
2025-12-04 18:38:23 +09:00
dohyeons cd39b2fc4d 배치 신규 생성 페이지 UI/UX 개선 2025-12-04 18:34:29 +09:00
kjs bc66f3bba1 거래처 에러수정 2025-12-04 18:26:35 +09:00
dohyeons ef3b85f343 배치 UPSERT 기능 및 고정값 매핑 버그 수정 2025-12-04 17:26:29 +09:00
kjs 93d9937343 자동완성 검색 입력 컴포넌트 다중 컬럼 표시 기능추가 2025-12-04 16:02:00 +09:00
SeongHyun Kim dfc83f6114 feat(split-panel-layout2): 테이블 모드, 수정/삭제, 복수 버튼 기능 추가
- 표시 모드 추가 (card/table)
- 카드 모드 라벨 표시 옵션 (이름 행/정보 행 가로 배치)
- 체크박스 선택 기능 (전체/개별 선택)
- 개별 수정/삭제 핸들러 구현 (openEditModal, DELETE API)
- 복수 액션 버튼 배열 지원 (add, edit, bulk-delete, custom)
- 설정 패널에 표시 라벨 입력 필드 추가
- 기본키 컬럼 설정 옵션 추가
2025-12-04 14:32:04 +09:00
kjs 2cddb42255 엔티티 표시기능 개선 2025-12-04 14:30:52 +09:00
kjs a90ddac512 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-04 13:55:28 +09:00
kjs 127f4dc783 숫자컬럼 천단위 구분자 설정 추가 2025-12-04 13:37:17 +09:00
SeongHyun Kim 40c43bab16 feat(numbering-rule): 채번규칙 구분자 설정 기능 추가
- SeparatorType 타입 및 SEPARATOR_OPTIONS 상수 추가
- 구분자 선택 UI 추가 (없음, -, _, ., /, 직접입력)
- 직접 입력 시 최대 2자 제한
- 새 규칙 생성 시 기본값 하이픈(-)
- Select 빈 문자열 에러 해결 (value: "" -> "none")
2025-12-04 13:28:13 +09:00
hjlee 7a2f80b646 Merge pull request '차량 아이콘 안뒤집히게' (#246) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/246
2025-12-04 10:46:57 +09:00
leeheejin 532c56f997 차량 아이콘 안뒤집히게 2025-12-04 10:46:37 +09:00
kjs 3ab32820e9 next.js 버전 15.4.8 2025-12-04 10:39:07 +09:00
hjlee 687a1d57b2 Merge pull request '지도 수정 및 경로확인 가능하게' (#245) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/245
2025-12-04 10:36:07 +09:00
leeheejin dbf6cfc995 지도 수정 및 경로확인 가능하게 2025-12-04 10:30:15 +09:00
kjs 4d9f010ac5 Merge pull request 'feature/screen-management' (#244) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/244
2025-12-03 19:11:37 +09:00
kjs 0d1be47914 Merge branch 'main' into feature/screen-management 2025-12-03 19:11:30 +09:00
kjs c39409823c Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-03 19:11:18 +09:00
kjs 714919ad64 모바일환경 2025-12-03 19:11:17 +09:00
kjs 0320d30f2d Merge pull request '헤더생성' (#243) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/243
2025-12-03 19:05:33 +09:00
kjs 0cb8d2cbe1 Merge branch 'main' into feature/screen-management 2025-12-03 19:05:25 +09:00
kjs 4569defecf 헤더생성 2025-12-03 19:05:10 +09:00
kjs 3ebc5ea557 Merge pull request '타입에러 수정' (#242) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/242
2025-12-03 18:56:26 +09:00
kjs e9738ce67f 타입에러 수정 2025-12-03 18:56:14 +09:00
SeongHyun Kim 669717f656 feat(split-panel-layout2): 복수 검색 컬럼 지원 기능 추가
- SearchColumnConfig 타입 추가 (types.ts)
- 좌측/우측 패널 모두 여러 검색 컬럼 설정 가능
- ConfigPanel에 검색 컬럼 추가/삭제 UI 구현
- 검색 시 OR 조건으로 여러 컬럼 동시 검색
- 기존 searchColumn 단일 설정과 하위 호환성 유지
2025-12-03 18:53:03 +09:00
SeongHyun Kim 52ad67d44a feat: SplitPanelLayout2 마스터-디테일 컴포넌트 구현
좌측 패널(마스터)-우측 패널(디테일) 분할 레이아웃 컴포넌트 추가
EditModal에 isCreateMode 플래그 추가하여 INSERT/UPDATE 분기 처리
dataFilter 기반 정확한 조인 필터링 구현
좌측 패널 선택 데이터를 모달로 자동 전달하는 dataTransferFields 설정 지원
ConfigPanel에서 테이블, 컬럼, 조인 설정 가능
2025-12-03 18:53:03 +09:00
SeongHyun Kim ca3d6bf8fb fix(split-panel-layout): 좌측 패널 표시 컬럼 설정이 반영되지 않던 문제 수정
- leftPanel.columns 설정을 우선 적용하도록 로직 변경
- 조인 키(leftColumn) 대신 사용자 설정 컬럼이 표시되도록 수정
- 컬럼 라벨 변환 로직 개선
2025-12-03 18:53:03 +09:00
kjs 8b3017224f Merge pull request 'feature/screen-management' (#241) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/241
2025-12-03 18:48:48 +09:00
kjs e8be871d69 Merge branch 'main' into feature/screen-management 2025-12-03 18:48:41 +09:00
SeongHyun Kim de8b643277 Merge remote-tracking branch 'origin/main' into ksh 2025-12-03 18:48:37 +09:00
kjs cb0bbd1ff3 카드디스플레이 검색필터 구현 2025-12-03 18:48:23 +09:00
SeongHyun Kim 294c61e0e3 feat(split-panel-layout2): 복수 검색 컬럼 지원 기능 추가
- SearchColumnConfig 타입 추가 (types.ts)
- 좌측/우측 패널 모두 여러 검색 컬럼 설정 가능
- ConfigPanel에 검색 컬럼 추가/삭제 UI 구현
- 검색 시 OR 조건으로 여러 컬럼 동시 검색
- 기존 searchColumn 단일 설정과 하위 호환성 유지
2025-12-03 18:43:01 +09:00
kjs 676ec16879 화면 분할패널 오류 수정 2025-12-03 18:28:43 +09:00
hyeonsu cb9c90fcdb Merge pull request 'common/feat/dashboard-map' (#240) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/240
2025-12-03 18:09:22 +09:00
dohyeons 6a0ff5582f Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-03 17:46:01 +09:00
SeongHyun Kim 700623aa78 feat: SplitPanelLayout2 마스터-디테일 컴포넌트 구현
좌측 패널(마스터)-우측 패널(디테일) 분할 레이아웃 컴포넌트 추가
EditModal에 isCreateMode 플래그 추가하여 INSERT/UPDATE 분기 처리
dataFilter 기반 정확한 조인 필터링 구현
좌측 패널 선택 데이터를 모달로 자동 전달하는 dataTransferFields 설정 지원
ConfigPanel에서 테이블, 컬럼, 조인 설정 가능
2025-12-03 17:45:22 +09:00
kjs 2a72f89c8a Merge pull request 'feature/screen-management' (#239) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/239
2025-12-03 17:36:52 +09:00
kjs 4e29f92268 테이블 타입관리 ui개선 2025-12-03 16:39:47 +09:00
kjs eb5ea411c9 화면 일괄삭제기능 2025-12-03 16:02:09 +09:00
SeongHyun Kim 760f9b2d67 fix(split-panel-layout): 좌측 패널 표시 컬럼 설정이 반영되지 않던 문제 수정
- leftPanel.columns 설정을 우선 적용하도록 로직 변경
- 조인 키(leftColumn) 대신 사용자 설정 컬럼이 표시되도록 수정
- 컬럼 라벨 변환 로직 개선
2025-12-03 15:17:43 +09:00
kjs 8317af92cd 입력 타입 변경시 바로 적용 가능하게 수정 2025-12-03 10:24:07 +09:00
kjs 37705e4a24 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-03 10:11:57 +09:00
kjs e83fbed71c 셀렉트 박스 카테고리 다른값 들어가는 오류 수정 2025-12-03 10:09:31 +09:00
kjs e33664015a 상단 헤더 제거 2025-12-03 10:03:24 +09:00
kjs 6982635acd Merge pull request 'feature/screen-management' (#238) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/238
2025-12-02 18:08:48 +09:00
kjs 7713d4073c Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-02 18:07:24 +09:00
kjs 3b875f20b1 화면간 데이터 전달기능 구현 2025-12-02 18:03:52 +09:00
SeongHyun Kim 3a3ecde358 Merge branch 'ksh' 2025-12-02 17:56:58 +09:00
SeongHyun Kim ae7b21147b feat(repeat-screen-modal): 집계 저장 및 채번 규칙 값 저장 기능 추가
- RepeatScreenModal 집계 결과를 연관 테이블에 저장하는 기능 추가
- ButtonPrimary 저장 시 채번 규칙 값(shipment_plan_no) 함께 저장
- _repeatScreenModal_* 데이터 감지 시 메인 테이블 중복 저장 방지
- 기존 행 수정 모드(_isEditing) 지원
- AggregationSaveConfig 타입 및 ConfigPanel UI 추가
2025-12-02 17:44:24 +09:00
dohyeons faacd5402c 외부 연결 목록에 회사명 표시 기능 추가 2025-12-02 17:36:28 +09:00
hjlee 70c6da0527 Merge pull request 'lhj' (#237) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/237
2025-12-02 15:34:41 +09:00
leeheejin 4c4906f6b3 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj 2025-12-02 15:33:55 +09:00
leeheejin a4f0681f76 지도 작동되게 했음 2025-12-02 15:33:45 +09:00
SeongHyun Kim 10d81cb9bc feat(repeat-screen-modal): 테이블 행 편집 모드 제어 기능 구현
- DB 로드 데이터에 _isEditing: false 명시적 설정
- handleEditExternalRow: 수정 모드 전환 함수 추가
- handleCancelEditExternalRow: 수정 취소 및 원본 복원 함수 추가
- renderTableCell: isRowEditable 파라미터 추가로 행 수준 편집 제어
- UPDATE API 요청 형식 { originalData, updatedData }로 수정
- 테이블 작업 컬럼에 수정/수정취소/삭제/복원 버튼 그룹화
2025-12-02 15:23:25 +09:00
SeongHyun Kim b286bc3c63 feat(repeat-screen-modal): 테이블 삭제 기능 DB 연동 (소프트 삭제)
- 삭제 버튼 클릭 시 _isDeleted 플래그 설정 (소프트 삭제)
- 삭제된 행 시각적 표시 (취소선, 투명도)
- 삭제 취소(복원) 기능 추가
- 저장 버튼 클릭 시 DELETE API 호출하여 DB 반영
- 삭제된 행 집계 계산에서 제외
- axios DELETE 요청 시 body 전달 방식 수정
2025-12-02 14:50:00 +09:00
SeongHyun Kim 8e257f36b2 fix: ScreenModal selectedData 로직 복원 (RepeatScreenModal 지원) 2025-12-02 14:30:29 +09:00
hjlee 7417f0e398 Merge pull request 'lhj' (#236) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/236
2025-12-02 14:27:50 +09:00
leeheejin a75b615c3a Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj 2025-12-02 14:26:03 +09:00
leeheejin 9078873240 워크플로우 restapi도 연결가능하고여러개 가능하게 구현시켜놓음 2025-12-02 14:24:43 +09:00
SeongHyun Kim bc34cded95 merge: origin/main을 ksh로 머지 (UnifiedPropertiesPanel 충돌 해결) 2025-12-02 14:10:33 +09:00
SeongHyun Kim 4787a8b177 feat(repeat-screen-modal): 테이블 영역 독립 저장 기능 구현
- TableCrudConfig에 allowSave, saveButtonLabel 속성 추가
- CRUD 설정 패널에 저장 스위치 추가
- saveTableAreaData 함수: editable 컬럼 + 조인키만 필터링하여 저장
- 날짜 필드 ISO 8601 -> YYYY-MM-DD 형식 변환
- 백엔드: company_code 자동 주입 로직 추가
- tableManagementService에 hasColumn 메서드 추가
2025-12-02 14:02:47 +09:00
hjlee a42db5f15a Merge pull request 'lhj' (#235) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/235
2025-12-02 13:21:26 +09:00
leeheejin 30e6595bf3 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj 2025-12-02 13:21:03 +09:00
leeheejin 2c447fd325 restapi도 가능하게 구현 2025-12-02 13:20:49 +09:00
dohyeons 436d604bb3 REST API 연결 생성 시 회사별 연결명 중복 허용 2025-12-02 11:12:09 +09:00
hjlee 650c5ef722 Merge pull request '공차관련수정사항들' (#234) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/234
2025-12-02 09:58:05 +09:00
leeheejin 0789eb2e20 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj 2025-12-02 09:53:18 +09:00
leeheejin 8c83db596d 공차관련수정사항들 2025-12-02 09:53:08 +09:00
dohyeons cd47f569e2 feat: 공차중계 운전자 차량/프로필 관리 기능 구현 2025-12-01 19:03:43 +09:00
SeongHyun Kim 2f78c83ef6 feat(repeat-screen-modal): 외부 테이블 조인, 필터링, CRUD 및 실시간 집계 기능 추가
- 외부 테이블 데이터 소스 설정 (TableDataSourceConfig) 추가
- 다중 테이블 조인 지원 (AdditionalJoinConfig)
- 테이블 필터링 (equals/notEquals) 지원
- 테이블 CRUD (행 추가/수정/삭제) 기능 추가
- 데이터 변경 시 집계 실시간 재계산 (recalculateAggregationsWithExternalData)
- 시각적 수식 빌더 (FormulaBuilder) 컴포넌트 추가
- 테이블 컬럼 순서 변경 기능 추가
- 백엔드: 배열 파라미터 IN 절 변환 로직 추가
2025-12-01 18:50:26 +09:00
dohyeons 9c3f1d26ad 차량관리(기초데이터) 구현 2025-12-01 18:41:02 +09:00
kjs 44c76d80b7 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-01 18:39:20 +09:00
kjs a12f2273b3 Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-01 18:39:02 +09:00
kjs fb16e224f0 카드 컴포넌트 중간커밋 2025-12-01 18:39:01 +09:00
SeongHyun Kim fb068284db Merge branch 'ksh' 2025-12-01 18:36:06 +09:00
SeongHyun Kim 0281d3722e revert: SelectBasicComponent.tsx 이전 상태로 복원 2025-12-01 18:35:55 +09:00
dohyeons cea2421899 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-01 17:21:02 +09:00
hjlee 18521339bb Merge pull request 'lhj' (#233) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/233
2025-12-01 17:05:34 +09:00
leeheejin 7242f08224 공차 등록, 연속추적 기능 2025-12-01 17:04:59 +09:00
leeheejin fbeb3ec2c9 버튼 과정이 조금 복잡하지만 위도경도 연속추적기능도 넣음 2025-12-01 16:49:02 +09:00
dohyeons 02273b2d79 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-01 15:52:28 +09:00
hjlee 15d5708b5d Merge pull request 'lhj' (#232) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/232
2025-12-01 15:46:13 +09:00
leeheejin 7263c9c3ff Merge origin/main into lhj - resolve buttonActions.ts conflict 2025-12-01 15:44:19 +09:00
leeheejin 6545410d49 공차등록 기능 구현 2025-12-01 15:42:40 +09:00
kjs 36132bf07c Merge pull request 'feature/screen-management' (#231) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/231
2025-12-01 15:30:43 +09:00
kjs aca00b8704 Merge branch 'main' into feature/screen-management 2025-12-01 15:30:38 +09:00
kjs 617655a42a Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-01 15:30:26 +09:00
kjs b1b9e4ad93 타입스크립트 에러 수정 2025-12-01 15:30:25 +09:00
leeheejin 8d2ec8e737 공차등록성공 2025-12-01 15:23:07 +09:00
kjs b77cc47791 Merge pull request 'feature/screen-management' (#230) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/230
2025-12-01 15:22:19 +09:00
kjs 1823415a5b Merge branch 'main' into feature/screen-management 2025-12-01 15:22:07 +09:00
kjs da6ac92391 데이터 수정이 안되는 문제 해결 2025-12-01 15:21:03 +09:00
dohyeons 4b06c6f83a 대시보드 뷰어 다운로드 버튼 비활성화(주석처리) 2025-12-01 15:04:52 +09:00
leeheejin be2550885a Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2025-12-01 14:45:51 +09:00
leeheejin fd7a1bbf53 출발지도착지 선택 가능하고 교환버튼 작동하게 2025-12-01 12:27:24 +09:00
hyeonsu 655eead3b6 Merge pull request 'common/feat/dashboard-map' (#229) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/229
2025-12-01 11:51:55 +09:00
dohyeons 848d111975 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-01 11:50:42 +09:00
dohyeons 75bdc19f25 배치 스케쥴러 함수명 오류 및 매핑 조회 누락 수정 2025-12-01 11:34:22 +09:00
kjs 93b92960e7 테이블 상단 여백 제거 2025-12-01 11:20:06 +09:00
dohyeons ad0a84f2c3 feat: 대시보드 목록에 생성자 컬럼 추가 2025-12-01 11:07:35 +09:00
leeheejin d7ee63a857 출발지 목적지 선택 2025-12-01 11:07:16 +09:00
dohyeons 64c11d548c 디지털 트윈 레이아웃 조회 시 최고 관리자 권한 처리 추가 2025-12-01 10:44:56 +09:00
kjs a3d3db5437 검색필터 다중선택 기능 2025-12-01 10:36:57 +09:00
leeheejin c657d6f7a0 출발지 도착지 2025-12-01 10:32:12 +09:00
dohyeons 53eab6ac9c 대시보드 목록/상세 조회 권한을 company_code 기반으로 변경 2025-12-01 10:30:47 +09:00
SeongHyun Kim 9e6fa67215 fix: 셀렉트 드롭다운이 다른 컴포넌트에 가려지는 문제 해결
- React Portal 적용하여 드롭다운을 document.body에 렌더링
- Stacking Context 탈출로 z-index 충돌 문제 해결
- 모든 셀렉트 타입(code, autocomplete, dropdown, multiselect)에 적용
2025-12-01 10:29:14 +09:00
kjs 142fb15dc0 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-12-01 10:21:34 +09:00
kjs e4b1f7e4d8 데이터 표시 오류 수정 2025-12-01 10:19:20 +09:00
dohyeons 1462700c83 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-12-01 10:15:10 +09:00
dohyeons ac01c7586d Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node 2025-12-01 10:14:47 +09:00
dohyeons 1849bf6654 대시보드 조회 시 company_code 기반 접근 권한으로 변경 2025-12-01 10:14:41 +09:00
kjs 1503dd87bb 화면 분할패널 수정모드 수정 2025-12-01 10:09:19 +09:00
SeongHyun Kim 893cd428a0 fix: 셀렉트 드롭다운이 다른 컴포넌트에 가려지는 문제 해결
- React Portal 적용하여 드롭다운을 document.body에 렌더링
- Stacking Context 탈출로 z-index 충돌 문제 해결
- 모든 셀렉트 타입(code, autocomplete, dropdown, multiselect)에 적용
2025-12-01 10:01:10 +09:00
hjlee 93174db7c8 Merge pull request '버튼 액션중 위치정보 가져오기, 필드값 변경 추가' (#228) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/228
2025-11-28 18:46:21 +09:00
leeheejin 9f97a16d6a Merge origin/main and resolve conflicts - add geolocation/update_field actions 2025-11-28 18:45:41 +09:00
kjs 84fee9cc38 Merge pull request 'feature/screen-management' (#227) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/227
2025-11-28 18:38:28 +09:00
kjs bd4e3e507d Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-28 18:38:18 +09:00
kjs 627c5a5173 화면 분할 패널 수정모드 기능 2025-11-28 18:35:34 +09:00
leeheejin 67e6a8008d 버튼 액션중 위치정보 가져오기, 필드값 변경 추가 2025-11-28 18:35:07 +09:00
dohyeons b43bf57ea9 불필요한 기능 주석처리 2025-11-28 17:47:09 +09:00
SeongHyun Kim 07e0b22309 Merge remote-tracking branch 'origin/main' into ksh 2025-11-28 16:03:10 +09:00
SeongHyun Kim 36ab484029 feat(repeat-screen-modal): 자유 레이아웃 구현 및 데이터 전달 버그 수정
- contentRows 기반 자유 레이아웃 지원 (header/aggregation/table/fields 타입)
- aggregationFields, tableColumns 직접 참조하도록 렌더링 로직 수정
- groupByField 없어도 grouping.enabled면 그룹핑 모드로 처리
- buttonActions에서 selectedRowsData를 모달 이벤트로 전달
- ScreenModal에서 selectedData를 groupedData props로 컴포넌트에 전달
- types.ts에 CardContentRowConfig, AggregationDisplayConfig 인터페이스 추가
2025-11-28 16:02:29 +09:00
kjs c78ba865b6 카테고리 설정 안풀리는 오류 수정 2025-11-28 15:15:35 +09:00
hjlee f1ff835a45 Merge pull request 'lhj' (#226) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/226
2025-11-28 15:06:10 +09:00
kjs f15846fd10 화면 분할 패널 기능 2025-11-28 14:56:11 +09:00
leeheejin 552beabdc0 null로 저장되게 성공시킴 2025-11-28 14:45:04 +09:00
leeheejin 652617fe37 주석처리완 2025-11-28 11:52:23 +09:00
SeongHyun Kim c94b9da813 feat: 신규 컴포넌트 2종 추가 (SimpleRepeaterTable, RepeatScreenModal) 및 속성 패널 스크롤 개선
- SimpleRepeaterTable: 검색/추가 없이 데이터 표시 및 편집, 자동 계산 지원
- RepeatScreenModal: 그룹핑 기반 카드 레이아웃, 집계 기능, 테이블 모드 지원
- UnifiedPropertiesPanel: overflow-x-auto 추가로 가로 스크롤 활성화
2025-11-28 11:48:46 +09:00
dohyeons 39d327fb45 외부 REST API 연결 확장 2025-11-28 11:35:36 +09:00
leeheejin 8dcffa8927 메일관련된거 커밋 2025-11-28 11:34:48 +09:00
hyeonsu ab734268a4 Merge pull request '이알솔루션 rest api 연결' (#225) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/225
2025-11-28 10:48:08 +09:00
dohyeons b70ed8aaff Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-11-28 10:47:55 +09:00
dohyeons 586dde96fb 외부 REST API 목록에 DB 토큰 인증 라벨 추가 2025-11-27 17:14:24 +09:00
dohyeons 2c5fe41a21 thiratis.com API 연결을 위한 SSL 인증서 검증 예외 처리 추가 2025-11-27 17:11:39 +09:00
dohyeons 7c42e88593 외부 REST API 커넥션에 DB 토큰 및 테스트 UX 개선 2025-11-27 17:11:30 +09:00
dohyeons f3c5c90d7b 외부 REST API 커넥션 POST/Body + DB 토큰 테스트 지원 2025-11-27 16:42:48 +09:00
kjs 30dac204c0 메뉴복사 기능수정(카테고리,코드값 제거) 2025-11-27 14:53:51 +09:00
kjs 51c49f7a3d 화면 분할패널 커밋 2025-11-27 12:54:57 +09:00
SeongHyun Kim 244c597ac9 refactor(admin): 테이블 타입 관리 Entity 조인 UI 레이아웃 개선
- Flexbox에서 Grid 레이아웃으로 변경 (160px 200px 250px 1fr)
- "상세 설정" 컬럼 제거하고 4개 컬럼 구조로 단순화
- Entity 조인 설정(참조/조인/표시 컬럼)을 입력 타입 컬럼 내 세로 배치
- Select 박스 너비를 192px (w-48)로 통일
- UI 겹침 현상 해결 및 순차적 설정 흐름 개선
2025-11-27 12:22:39 +09:00
kjs 454f79caec Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-27 12:08:33 +09:00
kjs fb9de05b00 화면 분할패널 중간커밋 2025-11-27 12:08:32 +09:00
dohyeons 5b98819191 토큰 배치 수정 화면에서 API 응답 미리보기 및 access_token 매핑 편집 가능하도록 개선 2025-11-27 12:08:18 +09:00
dohyeons 06c39df3a9 배치 수정 페이지 우상단 저장 버튼 삭제 2025-11-27 11:55:39 +09:00
dohyeons a7135b4c3c 배치 시 company_code도 저장되도록 2025-11-27 11:55:31 +09:00
dohyeons 25c2ab3413 배치 생성 페이지에 memo 사용 2025-11-27 11:48:03 +09:00
dohyeons 707328e765 REST API→DB 토큰 배치 및 auth_tokens 저장 구현 2025-11-27 11:32:19 +09:00
dohyeons ed56e14aa2 사용 완료된 mail-sent JSON 로그 정리 2025-11-27 11:31:52 +09:00
SeongHyun Kim a1117092aa feat: 수주일(order_date) 일괄 적용 기능 구현
- OrderItemRepeaterTable에 order_date 컬럼 추가
- ModalRepeaterTableComponent에 수주일 일괄 적용 로직 구현
- 원본 newData 참조로 납기일 로직과 독립적으로 작동
- 모든 행이 비어있는 초기 상태에서 첫 선택 시 자동 적용
- isOrderDateApplied 플래그로 1회만 실행 보장
2025-11-27 10:33:54 +09:00
SeongHyun Kim c7d47a6634 feat: 채번 규칙 자동/수동 모드 전환 기능 구현
- 사용자 수정 감지 시 자동으로 수동 모드 전환
- 원본 자동 생성 값 추적으로 모드 전환 기준 설정
- 수동 모드 시 채번 규칙 ID 제거하여 재할당 방지
- 원본 값 복구 시 자동 모드로 재전환 및 메타데이터 복구
2025-11-27 09:43:05 +09:00
SeongHyun Kim a9577a8f9a fix: 수주 등록 시 사용자가 수정한 수주번호 덮어쓰기 문제 해결
- 저장 시점에 채번 규칙 강제 재할당 로직 제거
- TextInputComponent에서 생성된 값을 사용자가 수정하면 그대로 유지
- allocateNumberingCode API 불필요한 호출 제거
- 사용자 입력 값 보존 및 순번 불필요 증가 방지
2025-11-26 18:24:15 +09:00
SeongHyun Kim b3e1e620da Merge remote-tracking branch 'origin/main' into ksh 2025-11-26 17:23:18 +09:00
SeongHyun Kim 13af9a62e8 fix: 수주관리 납기일 DATE 형식 저장 및 설정 패널 오류 수정
- 프론트엔드: EditModal 날짜 정규화 함수 추가 (YYYY-MM-DD)
- 백엔드: convertValueForPostgreSQL에서 DATE 타입 문자열 유지
- 백엔드: 날짜 변환 로직에서 YYYY-MM-DD 문자열 변환 제거
- 프론트엔드: ModalRepeaterTableConfigPanel prop 이름 통일 (onChange)
- OrderItemRepeaterTable 필드명 수정 (delivery_date → item_due_date)

closes #납기일-TIMESTAMP-저장-이슈 #설정패널-prop-오류
2025-11-26 17:22:39 +09:00
kjs 5405a7100d Merge pull request 'feature/screen-management' (#224) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/224
2025-11-26 16:10:13 +09:00
kjs 6ef4ff8e9b Merge branch 'main' into feature/screen-management 2025-11-26 16:10:06 +09:00
dohyeons 5787550cc9 에디터 속성 편집 성능 최적화 (디바운스 적용) 2025-11-26 16:05:33 +09:00
kjs e8c02fef5e 미리보기 기능 수정 2025-11-26 14:58:18 +09:00
kjs 13fe9c97fe 오류 수정 2025-11-26 14:44:49 +09:00
SeongHyun Kim 98a58368a6 Merge branch 'ksh' 2025-11-26 14:07:56 +09:00
SeongHyun Kim acc2a6169d style: EditModal 코드 포맷팅 및 불필요한 공백 제거
- trailing whitespace 정리
- 들여쓰기 일관성 유지
- 그룹 편집 안내 메시지 UI 제거
2025-11-26 14:05:22 +09:00
SeongHyun Kim 17659a0e59 Merge branch 'ksh' 2025-11-26 11:04:04 +09:00
SeongHyun Kim e4be76fe8d fix: 수주 등록 시 재질 컬럼 저장 오류 수정
- ModalRepeaterTableComponent의 저장 필터링 로직 개선
- columnMappings에 정의된 필드는 sourceColumns에 있어도 저장
- mappedFields 우선순위로 필터링 순서 변경
- 조인 전용 컬럼과 복사 저장 컬럼 구분 가능
2025-11-26 11:02:31 +09:00
SeongHyun Kim c0c81f20fc feat: 품목 수정 시 발생하는 타입 캐스팅 에러 해결
- ModalRepeaterTableComponent에 납기일 자동 일괄 적용 로직 구현
- 첫 납기일 선택 시 빈 행에 자동으로 동일 날짜 적용
- isDeliveryDateApplied 플래그로 중복 실행 방지
- ScreenModal 환경에서 onFormDataChange 경로 지원
- updateFormDataPartial에서 WHERE 조건의 PK 타입 동적 감지
- integer, numeric, uuid 등 다양한 타입에 대응
- ::text 하드코딩 제거하여 타입 불일치 에러 해결
2025-11-26 10:39:23 +09:00
SeongHyun Kim c387221043 feat: 품목 납기일 일괄 적용 기능 추가
- ModalRepeaterTableComponent에 납기일 자동 일괄 적용 로직 구현
- 첫 납기일 선택 시 빈 행에 자동으로 동일 날짜 적용
- isDeliveryDateApplied 플래그로 중복 실행 방지
- ScreenModal 환경에서 onFormDataChange 경로 지원
2025-11-26 10:07:38 +09:00
kjs 92fe72f4ed Merge pull request 'feature/screen-management' (#223) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/223
2025-11-26 09:33:15 +09:00
kjs 611fe9f788 선택박스 설정 2025-11-26 09:32:55 +09:00
kjs ea88cfd043 feat: 날짜 기간 검색 기능 구현
- ModernDatePicker: 로컬 상태 관리로 즉시 검색 방지
  - tempValue 상태 추가하여 확인 버튼 클릭 시에만 검색 실행
  - 빠른 선택 버튼 추가 (오늘, 이번주, 이번달, 최근 7일, 최근 30일)

- TableSearchWidget: ModernDatePicker 통합
  - 기본 HTML input[type=date]를 ModernDatePicker로 교체
  - 날짜 범위 객체 {from, to}를 파이프 구분 문자열로 변환
  - 백엔드 재시작 없이 작동하도록 임시 포맷팅 적용

- tableManagementService: 날짜 범위 검색 로직 개선
  - getColumnWebTypeInfo: web_type이 null이면 input_type 폴백
  - buildDateRangeCondition: VARCHAR 타입 날짜 컬럼 지원
  - 날짜 컬럼을 ::date로 캐스팅하여 타입 호환성 확보
  - 파이프 구분 문자열 파싱 지원 (YYYY-MM-DD|YYYY-MM-DD)

- 디버깅 로깅 추가
  - 컬럼 타입 정보 조회 결과 로깅
  - 날짜 범위 검색 조건 생성 과정 추적
2025-11-25 17:48:23 +09:00
SeongHyun Kim 0a6c5fbfcc fix: 수주관리 납기일 DATE 형식 저장 구현
- 프론트엔드: EditModal에 날짜 정규화 함수 추가 (YYYY-MM-DD 형식)
- 백엔드: convertValueForPostgreSQL에서 DATE 타입 문자열 그대로 유지
- 기존 TIMESTAMP 형식 변환을 DATE 타입 문자열 유지로 변경
- 날짜 변환 로직에서 YYYY-MM-DD 형식 문자열 변환 제거

closes #납기일-TIMESTAMP-형식-저장-이슈
2025-11-25 17:32:52 +09:00
dohyeons b2afe8674e 3D 뷰어 조명 설정 개선 (색상 왜곡 해결) 2025-11-25 17:23:24 +09:00
dohyeons f0513e20d8 3D 에디터 속성 입력 성능 최적화 2025-11-25 17:19:39 +09:00
dohyeons 710ca122ea STP 정차포인트를 자재 미적재 영역으로 분리하고 시각화 개선 2025-11-25 17:08:12 +09:00
SeongHyun Kim 8fdf57bedd chore: 과도한 콘솔 로그 정리
- ModalRepeaterTableComponent: 반복 렌더링 로그 제거
- TableListComponent: 렌더링 조건 체크 IIFE 단순화
- ConditionalContainerComponent: 디버깅 로그 삭제
- DynamicComponentRenderer: value 설정 로그 제거
- resizable-dialog: userStyle 상세 로그 정리
- page.tsx: 반복 데이터 탐색 로그 삭제

에러 핸들링 및 주요 분기점 로그만 보존
2025-11-25 16:56:50 +09:00
kjs 629be13816 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-25 16:14:15 +09:00
hjlee 11782536f4 Merge pull request '스크롤 커밋' (#222) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/222
2025-11-25 16:13:48 +09:00
leeheejin 6669a3fc5e 스크롤 커밋 2025-11-25 16:13:31 +09:00
kjs ef0af26147 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-25 16:05:56 +09:00
kjs a1819e749c fix: 탭 컴포넌트 menuObjid 전달, 카테고리 필터 복원, 설정 초기화 문제 해결
주요 수정사항:

1. 탭 컴포넌트 내 자식 화면에 menuObjid와 tableName 전달
   - TabsWidget에 menuObjid prop 추가
   - InteractiveScreenViewerDynamic를 통해 자식 화면에 전달
   - 채번 규칙 생성 시 올바른 메뉴 스코프 및 테이블명 적용

2. 백엔드: 화면 레이아웃 API에 tableName 추가
   - screenManagementService.getLayout()에서 테이블명 반환
   - LayoutData 타입에 tableName 필드 추가
   - 채번 규칙 생성 시 tableName 검증 강화

3. 카테고리 필터링 기능 복원
   - DataFilterConfigPanel에 menuObjid 전달
   - getCategoryValues API 사용으로 메뉴 스코프 적용
   - 새로고침 후 카테고리 값 자동 재로드
   - SplitPanelLayoutConfigPanel에 menuObjid 전달

4. 선택항목 상세입력 설정 패널 포커스 문제 해결
   - 로컬 입력 상태 추가로 실시간 속성 편집 패턴 적용
   - 텍스트 및 라벨 입력 시 포커스 유지

5. 테이블 리스트 설정 초기화 문제 해결
   - handleChange 함수에서 기존 config와 병합하여 전달
   - 다른 속성 손실 방지 (columns, dataFilter 등)

버그 수정:
- 채번 규칙 생성 시 빈 문자열 대신 null 전달
- 필터 설정 변경 시 컬럼 설정 초기화 방지
- 카테고리 컬럼 선택 시 셀렉트박스 표시
2025-11-25 15:55:05 +09:00
SeongHyun Kim 6317ae7b0b Merge remote-tracking branch 'origin/main' into ksh 2025-11-25 15:26:29 +09:00
SeongHyun Kim 2b8a3945a1 fix: Section Paper 선택 영역과 컨텐츠 영역 정렬 문제 해결
- RealtimePreview: border → outline 전환, getHeight() 함수 추가
- SectionPaperComponent: width/height 100%, overflow-auto, min-h 제거
- 모든 높이에서 선택 영역 = 컨텐츠 영역 정확히 일치
2025-11-25 15:22:50 +09:00
hyeonsu 50545a4570 Merge pull request '3d 변경사항' (#221) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/221
2025-11-25 15:07:37 +09:00
dohyeons f59218aa43 3d필드로 텍스트 변경 2025-11-25 15:06:55 +09:00
dohyeons 60832e88ff 3d필드 생성으로 변경 2025-11-25 15:01:47 +09:00
dohyeons d6b9372e1f Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-11-25 14:58:39 +09:00
dohyeons 080188b419 외부 DB 연결 설정 및 쿼리 처리 로직 보완 2025-11-25 14:57:48 +09:00
SeongHyun Kim e456b4bb69 Merge remote-tracking branch 'origin/main' into ksh 2025-11-25 14:26:57 +09:00
SeongHyun Kim 5609e32daf feat: 수주관리 품목 CRUD 및 공통 필드 자동 복사 구현
- 품목 추가 시 공통 필드(거래처, 담당자, 메모) 자동 복사
- ModalRepeaterTable onChange 시 groupData 반영
- 백엔드 타입 캐스팅으로 PostgreSQL 에러 해결
- 타입 정규화로 불필요한 UPDATE 방지
- 수정 모달에서 거래처/수주번호 읽기 전용 처리
2025-11-25 14:23:54 +09:00
dohyeons ace80be8e1 N-Level 계층 구조 및 공간 종속성 시스템 구현 2025-11-25 13:55:00 +09:00
SeongHyun Kim aca39f23d2 Merge branch 'ksh' 2025-11-25 13:15:13 +09:00
SeongHyun Kim d04330283a Merge remote-tracking branch 'origin/main' into ksh 2025-11-25 13:14:05 +09:00
kjs 7a52cf76d3 Merge pull request 'feature/screen-management' (#220) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/220
2025-11-25 13:05:27 +09:00
kjs 943d00bbbd Merge branch 'main' into feature/screen-management 2025-11-25 13:05:20 +09:00
kjs a0180d66a2 편집기 인풋 오류 수정 및 탭 컴포넌트 완성 2025-11-25 13:04:58 +09:00
SeongHyun Kim a9f57add62 feat: 수주관리 품목 추가/수정/삭제 기능 구현
- EditModal의 handleSave가 button-primary까지 전달되도록 수정
- ConditionalContainer/ConditionalSectionViewer에 onSave prop 추가
- DynamicComponentRenderer와 InteractiveScreenViewerDynamic에 onSave 전달 로직 추가
- ButtonActionExecutor에서 context.onSave 콜백 우선 실행 로직 구현
- 신규 품목 추가 시 groupByColumns 값 자동 포함 처리

기능:
- 품목 추가: order_no 자동 설정
- 품목 수정: 변경 필드만 부분 업데이트
- 품목 삭제: originalGroupData 비교 후 제거
2025-11-25 12:07:14 +09:00
kjs 5e2392c417 탭 컴포넌트 구현 2025-11-25 10:06:56 +09:00
dohyeons 6fe708505a Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-11-25 09:53:36 +09:00
dohyeons f10ceb5f7c Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node 2025-11-25 09:48:18 +09:00
dohyeons 119afcaf42 배치된 객체 목록 계층구조 및 아코디언 적용 2025-11-25 09:35:47 +09:00
kjs a46a2a664f Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-25 09:34:59 +09:00
kjs 9fda390c55 커밋 2025-11-25 09:34:44 +09:00
kjs 3f60f9ca3e fix(flow): 제어 실행 시 writer와 company_code 자동 입력 기능 추가
🐛 문제:
- 제어(플로우) 실행으로 데이터 INSERT 시 writer, company_code 컬럼이 비어있는 문제
- 플로우 실행 API에 인증이 없어 사용자 정보를 사용할 수 없었음

 해결:
1. 플로우 실행 API에 authenticateToken 미들웨어 추가
2. 사용자 정보(userId, userName, companyCode)를 contextData에 포함
3. INSERT 노드 실행 시 writer와 company_code 자동 추가
   - 필드 매핑에 없는 경우에만 자동 추가
   - writer: 현재 로그인한 사용자 ID
   - company_code: 현재 사용자의 회사 코드
   - 최고 관리자(companyCode = '*')는 제외

4. 플로우 제어 자동 감지 개선
   - flowConfig가 있으면 controlMode 없이도 플로우 모드로 인식
   - 데이터 미선택 시 명확한 오류 메시지 표시

🎯 영향:
- 입고처리, 출고처리 등 제어 기반 데이터 생성 시 멀티테넌시 보장
- 데이터 추적성 향상 (누가 생성했는지 자동 기록)

📝 수정 파일:
- frontend/lib/utils/buttonActions.ts
- backend-node/src/routes/dataflow/node-flows.ts
- backend-node/src/services/nodeFlowExecutionService.ts
2025-11-25 09:33:36 +09:00
dohyeons 216e1366ef 편집 시 기존 세팅 가져오는 로직 구현 2025-11-24 18:23:00 +09:00
dohyeons 711f2670de 초기 배치 시 프리뷰 생성 2025-11-24 18:16:15 +09:00
kjs 00501f359c 탭기능 중간커밋 2025-11-24 17:24:47 +09:00
dohyeons b80d6cb85e 영역의 자재를 “해당 영역”에만 배치가 가능하게 구현 2025-11-24 17:02:22 +09:00
SeongHyun Kim 1139cea838 feat(table-list): 컬럼 너비 자동 조정 및 정렬 상태 저장 기능 추가
- 데이터 내용 기반 컬럼 너비 자동 계산 (상위 50개 샘플링)
- 사용자가 조정한 컬럼 너비를 localStorage에 저장/복원
- 정렬 상태(컬럼, 방향)를 localStorage에 저장/복원
- 사용자별, 테이블별 독립적인 설정 관리

변경:
- TableListComponent.tsx: calculateOptimalColumnWidth 추가, 정렬 상태 저장/복원 로직 추가
- README.md: 새로운 기능 문서화

저장 키:
- table_column_widths_{테이블}_{사용자}: 컬럼 너비
- table_sort_state_{테이블}_{사용자}: 정렬 상태

Fixes: 수주관리 화면에서 컬럼 너비 수동 조정 번거로움, 정렬 설정 미유지 문제
2025-11-24 16:54:31 +09:00
dohyeons 90b7c2b0f0 자재 개수에 따른 높이 조절 2025-11-24 16:52:22 +09:00
SeongHyun Kim 51872de821 Merge remote-tracking branch 'origin/main' into ksh 2025-11-24 16:03:53 +09:00
SeongHyun Kim d10e00c044 fix: 수주관리 수정 저장 시 조인 컬럼 필터링 추가
- entityJoinApi 조회 데이터의 조인 컬럼(material_label 등) 필터링
- dynamicFormService.ts: 병합 모드에서 columnInfo 기반 유효 컬럼만 저장
- sales_order_mng 테이블에 존재하지 않는 컬럼 INSERT 방지
- column does not exist PostgreSQL 에러 해결

영향: 수주관리 그룹 편집 저장 정상 동작
2025-11-24 16:01:29 +09:00
dohyeons 7994b2a72a 계층 구조 유효성 검사 및 그룹 이동 기능 구현 2025-11-24 15:57:28 +09:00
hjlee b6eb66d9cb Merge pull request '리사이징 유지와 연속작성 구현 모달 살짝 늘어나는 문제 해결' (#219) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/219
2025-11-24 15:54:48 +09:00
leeheejin f286b6c695 리사이징 유지와 연속작성 구현 모달 살짝 늘어나는 문제 해결 2025-11-24 15:52:45 +09:00
SeongHyun Kim 2f3d5f993a fix: 수주관리 수정 저장 시 조인 컬럼 필터링 추가
- entityJoinApi 조회 데이터의 조인 컬럼(material_label 등) 필터링
- dynamicFormService.ts: 병합 모드에서 columnInfo 기반 유효 컬럼만 저장
- sales_order_mng 테이블에 존재하지 않는 컬럼 INSERT 방지
- "column does not exist" PostgreSQL 에러 해결

영향: 수주관리 그룹 편집 저장 정상 동작
2025-11-24 15:38:41 +09:00
SeongHyun Kim 3e414b8530 feat: 수주관리 그룹 편집 기능 구현
- 같은 수주번호(order_no)를 가진 품목 일괄 수정 기능 추가
- groupByColumns 개념 도입 및 EditModal 그룹 데이터 처리 로직 구현
- ConditionalSectionViewer에서 DynamicComponentRenderer 사용으로 groupedData 전달 경로 확보
- ModalRepeaterTable onChange 에러 수정 및 sourceColumns 필터링 추가
- 조인된 컬럼 제외 로직 추가로 DB 저장 오류 해결
2025-11-24 15:24:31 +09:00
kjs ddb1d4cf60 화면 좌우 맞추기 2025-11-24 12:02:23 +09:00
kjs 0c94c4cd5e Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-24 10:02:58 +09:00
kjs 1d1597f8e7 메뉴 수정 안되는 현상 수정 2025-11-24 10:02:56 +09:00
SeongHyun Kim 55204dd38c Merge remote-tracking branch 'origin/main' into ksh 2025-11-21 17:01:23 +09:00
SeongHyun Kim 58427fb8e0 style(ui): 배경색 통일 및 스크롤 최적화
- autocomplete-search-input: !bg-background 강제 적용
- section-paper: 배경색 진하게(bg-muted/40), 기본 테두리 활성화, overflow-visible
- modal-repeater-table: tbody 흰색 배경, 스크롤 높이 제한(240px), 헤더 고정
- autocomplete 드롭다운: z-index 100으로 상향

배경색 통일로 일관된 디자인, 스크롤로 공간 효율 개선
2025-11-21 17:00:25 +09:00
kjs 48b3687e41 Merge pull request 'feature/screen-management' (#218) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/218
2025-11-21 16:24:29 +09:00
kjs 73cd23aee8 Merge branch 'main' into feature/screen-management 2025-11-21 16:24:21 +09:00
kjs c1e5a2a5f1 fix: Select Basic 다중선택 높이 적용 및 menu_objid=0 타입 처리
1. Select Basic 다중선택 컴포넌트 높이 문제 해결
   - 외부 wrapper에 height: 100% 추가
   - 내부 div에 인라인 스타일로 height: 100% 명시
   - items-center 추가하여 태그 세로 가운데 정렬
   - Tailwind h-full 클래스 제거로 스타일 충돌 방지

2. 메뉴 복사 시 menu_objid=0 공통 카테고리 타입 처리
   - menu_objid가 숫자 0, 문자열 '0' 모두 처리
   - == 0 타입 강제 변환으로 모든 경우 감지
   - 카테고리 컬럼 매핑, 카테고리 값 모두 적용
   - 공통 카테고리 19개 정상 복사 가능
2025-11-21 16:23:37 +09:00
dohyeons 68e8e7b36b 초기에 설정 데이터 불러와지지 않는 현상 해결 2025-11-21 16:13:50 +09:00
kjs 3355ff4563 fix: menu_objid=0 공통 카테고리 값 스킵 문제 해결
문제:
- menu_objid = 0인 공통 카테고리 값들이 19개 스킵됨
- '⏭️  매핑할 메뉴가 없음: menu_objid=0' 로그 반복

원인:
- 삼항 연산자로 0을 할당했으나, 이후 if (newMenuObjid === undefined) 체크에서
- 0이 falsy 값이 아닌데도 undefined와 비교하여 통과하지 못함
- 실제로는 newMenuObjid가 0일 때도 continue되어 스킵됨

해결:
- menu_objid = 0일 경우를 명시적으로 처리
- 0인 경우 바로 0을 할당하고 continue 없이 진행
- 0이 아닌 경우만 menuIdMap에서 찾고, undefined 체크

변경 전:
const newMenuObjid = value.menu_objid === 0 ? 0 : menuIdMap.get(value.menu_objid);
if (newMenuObjid === undefined) continue; // 0도 여기서 걸림!

변경 후:
if (value.menu_objid === 0) {
  newMenuObjid = 0; // 공통 설정은 그대로 0
} else {
  newMenuObjid = menuIdMap.get(value.menu_objid);
  if (newMenuObjid === undefined) continue; // 진짜 undefined만 스킵
}

영향:
- 공통 카테고리 값 19개 정상 복사
- customer_mng, item_info의 division, status, currency_code 등 정상 동작
2025-11-21 16:10:55 +09:00
kjs be48d30d8f fix: 공통 카테고리 설정(menu_objid=0) 복사 누락 문제 해결
문제:
- menu_objid = 0인 공통 카테고리 값들이 복사되지 않음
- 원본 34개 중 15개만 복사됨 (19개 누락)
- customer_mng, item_info 등의 공통 카테고리 값들이 프론트엔드에서 안 보임

원인:
- collectCategorySettings: menu_objid로만 WHERE 필터링
- copyCategorySettings: menuIdMap.get()이 0을 찾지 못함

해결:
1. collectCategorySettings 함수:
   - WHERE menu_objid = ANY($1) OR menu_objid = 0
   - 공통 카테고리 설정도 함께 수집

2. copyCategorySettings 함수:
   - menu_objid = 0일 경우 그대로 0으로 유지
   - if (newMenuObjid === undefined) 체크로 안전성 강화

영향:
- 공통 카테고리 값(division, status, currency_code 등) 정상 복사
- 모든 화면에서 카테고리 값 정상 표시

테스트:
- 원본 34개 → 복사본 34개 (100% 복사)
- customer_mng.division, item_info.division 등 정상 동작
2025-11-21 16:04:04 +09:00
kjs 42435193cf fix: 덮어쓰기 시 외래키 제약조건 위반 문제 해결
문제:
- 기존 메뉴 삭제 시 numbering_rules.fk_numbering_rules_menu 외래키 제약조건 위반
- category_column_mapping.fk_mapping_menu 외래키 제약조건도 위반 가능

원인:
- 채번 규칙과 카테고리 설정이 menu_objid를 참조하는데, 메뉴를 먼저 삭제하려고 함

해결:
deleteExistingCopy 함수의 삭제 순서 변경:
1. 화면 레이아웃
2. 화면-메뉴 할당
3. 화면 정의
4. 메뉴 권한
5. 채번 규칙 파트 (추가)
6. 채번 규칙 (추가)
7. 테이블 컬럼 카테고리 값 (추가)
8. 카테고리 컬럼 매핑 (추가)
9. 메뉴 (역순)

관련 파일:
- backend-node/src/services/menuCopyService.ts

테스트:
- 메뉴 덮어쓰기 재복사 시 외래키 제약조건 위반 없이 정상 동작
2025-11-21 15:59:55 +09:00
kjs 10526da1ac feat: 카테고리 설정 덮어쓰기 모드로 변경
기존 동작:
- 카테고리 컬럼 매핑: 중복 시 스킵
- 카테고리 값: 중복 시 스킵
- 결과: 일부 값만 복사되어 불완전

새로운 동작 (덮어쓰기):
- 카테고리 컬럼 매핑: 기존 것 삭제 후 재생성
- 카테고리 값: 테이블+컬럼 단위로 기존 것 전체 삭제 후 재생성
- 부모-자식 관계는 유지 (depth 순으로 정렬 후 복사)

장점:
1. 메뉴 재복사 시 항상 최신 카테고리 설정으로 덮어씀
2. 누락된 값 없이 완전한 복사 보장
3. 테스트 시 기존 데이터 정리 불필요

주의사항:
- 기존 카테고리 값이 다른 데이터에서 참조되는 경우 외래키 제약조건 위반 가능
- 실무에서는 사용자 선택 옵션(덮어쓰기/병합)을 추가하는 것이 안전

관련 파일:
- backend-node/src/services/menuCopyService.ts

테스트:
- COMPANY_11로 재복사 시 모든 카테고리 값 정상 복사됨
2025-11-21 15:58:00 +09:00
dohyeons dd913d3ecf 3d에서 테이블 가져올 때 테이블, 컬럼 코멘트 같이 가져오기 2025-11-21 15:44:52 +09:00
kjs 8b3593c8fb feat: 메뉴 복사 시 화면명 일괄 변환 기능 추가
새로운 기능:
- 화면명에서 특정 텍스트 제거 (예: '탑씰' 제거)
- 화면명에 접두사 추가 (예: '한신' 추가)
- 변환 로직: 제거 → 접두사 추가 순서로 적용

백엔드:
- menuCopyService.copyMenu()에 screenNameConfig 파라미터 추가
- copyScreens()에서 화면명 변환 로직 적용
- 정규식으로 전역 치환 (new RegExp(text, 'g'))

프론트엔드:
- MenuCopyDialog에 화면명 일괄 변경 UI 추가
- Checkbox로 기능 활성화/비활성화
- 2개 Input: removeText, addPrefix
- API 호출 시 screenNameConfig 전달

사용 예시:
1. '탑씰 회사정보' → '회사정보' (제거만)
2. '회사정보' → '한신 회사정보' (접두사만)
3. '탑씰 회사정보' → '한신 회사정보' (제거 + 접두사)

관련 파일:
- backend-node/src/services/menuCopyService.ts
- backend-node/src/controllers/adminController.ts
- frontend/lib/api/menu.ts
- frontend/components/admin/MenuCopyDialog.tsx
2025-11-21 15:38:59 +09:00
kjs 14802f507f feat: 카테고리 설정 및 채번 규칙 복사 기능 추가
새로운 기능:
1. 카테고리 컬럼 매핑(category_column_mapping) 복사
2. 테이블 컬럼 카테고리 값(table_column_category_values) 복사
3. 채번 규칙(numbering_rules) 복사
4. 채번 규칙 파트(numbering_rule_parts) 복사

중복 처리:
- 모든 항목: 스킵(Skip) 정책 적용
- 이미 존재하는 데이터는 덮어쓰지 않고 건너뜀
- 카테고리 값: 부모-자식 관계 유지를 위해 기존 ID 매핑 저장

채번 규칙 특징:
- 구조(파트)는 그대로 복사
- 순번(current_sequence)은 1부터 초기화
- rule_id는 타임스탬프 기반으로 새로 생성 (항상 고유)

복사 프로세스:
- [7단계] 카테고리 설정 복사
- [8단계] 채번 규칙 복사

결과 로그:
- 컬럼 매핑, 카테고리 값, 규칙, 파트 개수 표시
- 스킵된 항목 개수도 함께 표시

이제 메뉴 복사 시 카테고리와 채번 규칙도 함께 복사되어
복사한 회사에서 바로 업무를 시작할 수 있습니다.

관련 파일:
- backend-node/src/services/menuCopyService.ts
2025-11-21 15:27:54 +09:00
kjs 3be98234a8 fix: conditional-container sections 경로 수정
문제:
- extractReferencedScreens()에서 props.sections를 체크
- 실제 데이터 구조는 props.componentConfig.sections
- 결과: conditional-container 안의 화면들이 수집되지 않음
- 예: 화면 205의 sections에 있는 202, 208 누락

해결:
- props.sections → props.componentConfig.sections
- conditional-container 안의 모든 화면 정상 수집
- 재귀 복사 로직은 이미 완벽하게 작동 중

참고:
- 모달 안의 모달(재귀 참조)은 이미 정상 작동
- 예: 157 → 253 → 254 (3단계 재귀) 

관련 파일:
- backend-node/src/services/menuCopyService.ts
2025-11-21 15:17:38 +09:00
kjs 9b7416b6f8 fix: source_menu_objid 저장 문제 해결 - BigInt 타입 비교 수정
문제:
- PostgreSQL BIGINT 타입이 JavaScript에서 문자열로 반환됨
- menu.objid === rootMenuObjid 비교가 항상 false (타입 불일치)
- 결과: source_menu_objid가 NULL로 저장되어 덮어쓰기 기능 작동 안 함

해결:
- String() 변환 후 비교: String(menu.objid) === String(rootMenuObjid)
- 타입에 관계없이 값 비교 가능
- source_menu_objid 정상 저장되어 덮어쓰기 기능 작동

검증:
- 로그: '📌 source_menu_objid 저장: 1762407678882 (원본 최상위 메뉴)'
- DB: menu_info.source_menu_objid = 1762407678882 

관련 파일:
- backend-node/src/services/menuCopyService.ts
2025-11-21 14:58:57 +09:00
kjs c70998fa4f feat: 메뉴 복사 기능 - 2단계 복사 방식으로 화면 참조 매핑 문제 해결
- 문제: 화면 복사 시 참조되는 화면이 아직 복사되지 않아 screenIdMap에 매핑 정보가 없었음
- 해결: 2단계 복사 방식 도입
  1단계: 모든 screen_definitions 먼저 복사하여 screenIdMap 완성
  2단계: screen_layouts 복사하면서 완성된 screenIdMap으로 참조 업데이트
- 결과: targetScreenId가 올바르게 새 회사의 화면 ID로 매핑됨 (예: 149 → 517)
- 추가: 화면 수집 시 문자열 타입 ID도 올바르게 파싱하도록 개선
- 추가: 참조 화면 발견 및 업데이트 로그 추가

관련 파일:
- backend-node/src/services/menuCopyService.ts
- db/migrations/1003_add_source_menu_objid_to_menu_info.sql
- db/scripts/cleanup_company_11_*.sql
2025-11-21 14:37:09 +09:00
dohyeons 6ccaa85413 우측패널 표시 컬럼 인풋 다 지우면 초기값이 들어오는 문제 해결 2025-11-21 14:29:17 +09:00
SeongHyun Kim da0a82a0ec Merge remote-tracking branch 'origin/main' into ksh 2025-11-21 12:23:15 +09:00
dohyeons 1e1bc0b2c6 대시보드 설정 저장 및 디지털 트윈 UX 개선 2025-11-21 12:22:27 +09:00
SeongHyun Kim 147f910c88 refactor(order): 백엔드 데이터 구조 개선, 미사용 UI 제거
백엔드:
- order_mng_master + order_mng_sub LEFT JOIN 조회
- json_agg로 품목 배열화 (items: OrderItem[])
- 타입 정의 추가 (order.ts)

프론트엔드:
- 미사용 그룹화 UI 컴포넌트 삭제
- 평면 테이블 형태 유지 (기존 UI 그대로)

Modified:
- backend-node/src/controllers/orderController.ts
2025-11-21 12:17:29 +09:00
SeongHyun Kim 1a171d450c fix: Repeater 동일 테이블 저장 시 이중 INSERT 문제 해결
문제:
- targetTable이 메인 테이블과 동일할 때 헤더 단독 저장 + Repeater 병합 저장으로 2번 INSERT 발생
- 같은 수주번호로 헤더만 있는 레코드와 전체 데이터 레코드가 중복 생성됨

해결:
- Repeater를 병합/분리 모드로 분류하는 로직 추가
- 병합 모드: 헤더+품목을 통합하여 품목당 1개 레코드로 저장
- 분리 모드: 헤더와 품목을 별도 테이블에 저장
- 헤더 단독 INSERT 제거로 중복 방지

영향:
- 단일 테이블 구조에서 품목별 레코드 생성 방식으로 변경
- 확장/축소 UI를 통한 품목별 조회 지원
2025-11-21 10:52:51 +09:00
hyeonsu 8727ef02f3 Merge pull request '대시보드 에러 해결' (#217) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/217
2025-11-21 10:30:25 +09:00
dohyeons 3d32929c0b 대시보드 에러 해결 2025-11-21 10:29:47 +09:00
SeongHyun Kim fa426625cc feat: modal-repeater-table 배열 데이터 저장 기능 구현
- 백엔드: 배열 객체 형식 Repeater 데이터 처리 로직 추가
- 백엔드: Repeater 저장 시 company_code 자동 주입
- 백엔드: 부모 테이블 데이터 자동 병합 (targetTable = tableName)
- 프론트엔드: beforeFormSave 이벤트로 formData 주입
- 프론트엔드: _targetTable 메타데이터 전달
- 프론트엔드: ComponentRendererProps 상속 및 Renderer 단순화

멀티테넌시 및 부모-자식 관계 자동 처리로
복잡한 배열 데이터 저장 안정성 확보
2025-11-21 10:12:29 +09:00
kjs bb49073bf7 feat: 테이블 리스트에서 카테고리 다중 값 배지 표시 지원
문제:
- 테이블에서 'CATEGORY_218152,CATEGORY_205381' 같은 다중 값이
- 배지로 표시되지 않고 코드값 그대로 보임

원인:
- formatCellValue의 카테고리 렌더링이 단일 값만 처리
- 콤마로 구분된 다중 값 파싱 로직 없음

해결:
1. 콤마 구분자 감지 및 값 배열로 분리
2. 단일 값: 기존 로직 유지 (단일 배지)
3. 다중 값: flex-wrap gap-1로 여러 배지 렌더링
4. 각 배지는 매핑된 라벨과 색상 사용

결과:
 다중선택 저장된 데이터가 테이블에서 여러 배지로 표시됨
 각 배지에 올바른 색상과 라벨 적용
 단일 값도 기존처럼 정상 작동
2025-11-21 10:03:26 +09:00
kjs f4d27f51a3 fix: 카테고리 값 매핑을 올바른 속성명으로 수정
문제:
- value: undefined, label: undefined
- 잘못된 속성명 사용 (categoryValue, categoryLabel)

원인:
- API 응답 실제 구조:
  - valueCode: 'CATEGORY_154396'
  - valueLabel: '대기'

해결:
- v.categoryValue → v.valueCode
- v.categoryLabel → v.valueLabel

이제 다중선택 카테고리 select가 완벽히 작동합니다:
 다중선택 모드 활성화
 카테고리 옵션 로딩
 라벨 정상 표시
 콤마 구분자로 저장
2025-11-21 09:40:24 +09:00
kjs 114a807d79 debug: 카테고리 API 응답 원본 데이터 구조 확인
문제:
- value: undefined, label: undefined로 나옴
- v.categoryValue, v.categoryLabel이 존재하지 않음

디버깅:
- API 응답의 첫 번째 항목 전체 출력
- 객체의 모든 키 목록 출력
- 여러 가능한 속성명 시도:
  - category_value / categoryValue / value
  - category_label / categoryLabel / label

다음 단계:
- 콘솔에서 원본 데이터 구조 확인
- 실제 속성명에 맞게 매핑 수정
2025-11-21 09:39:09 +09:00
kjs 4928c54985 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-21 09:29:23 +09:00
dohyeons 86c671026b test entity search페이지 임시 비활성화 2025-11-21 04:05:42 +09:00
dohyeons 4cbfa8d70d 임시비활성화 2025-11-21 03:58:02 +09:00
dohyeons ef08c3fd7a 타입에러 수정 2025-11-21 03:50:45 +09:00
dohyeons 96401634b2 타입 정의 삭제 2025-11-21 03:45:51 +09:00
dohyeons fda7614d48 빌드에러해결 2025-11-21 03:40:41 +09:00
hyeonsu 25c36167c0 Merge pull request '대시보드 수정' (#216) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/216
2025-11-21 03:34:27 +09:00
dohyeons 205d062f4a 수정.. 2025-11-21 03:33:49 +09:00
dohyeons 0450390b2a 배치 대략적인 완료 2025-11-21 02:25:25 +09:00
kjs fc16f27640 feat: select-basic에 카테고리(category) 타입 옵션 로딩 추가
문제:
- select-basic이 webType='category'일 때 옵션이 안 보임
- CATEGORY_218152 같은 코드값만 표시됨
- 체크박스는 보이지만 라벨이 비어있음

원인:
- select-basic은 useCodeOptions만 사용 (code 타입용)
- category 타입은 getCategoryValues API 필요

해결:
1. categoryOptions 상태 추가
2. webType === 'category'일 때 getCategoryValues 호출
3. getAllOptions에 categoryOptions 포함
4. 로딩 상태에 isLoadingCategories 추가

디버깅:
- 카테고리 로딩 시작/완료 로그
- API 응답 로그
- 최종 allOptions 로그 추가

다음 단계:
- 콘솔에서 categoryOptions가 제대로 로드되는지 확인
2025-11-20 18:35:48 +09:00
kjs dd568b7235 fix: select-basic 카테고리 조건 로직 수정
문제:
- 이전 커밋에서 로직을 반대로 작성
- componentType !== 'select-basic'로 했지만
- componentType === 'select-basic'일 때 건너뛰어야 함

수정:
- componentType === 'select-basic'이면 통과 (아무것도 안 함)
- 그 외 카테고리는 CategorySelectComponent 사용

로직:
if (category && componentType === 'select-basic') {
  // 통과 - ComponentRegistry로 진행
} else if (category) {
  // CategorySelectComponent 사용
}
2025-11-20 18:31:50 +09:00
kjs 87fbf5b858 fix: select-basic 컴포넌트가 CategorySelectComponent에 가로채지는 문제 해결
문제:
- componentType이 'select-basic'이지만 webType이 'category'일 때
- DynamicComponentRenderer가 무조건 CategorySelectComponent 사용
- select-basic의 multiple 설정이 무시됨

원인:
- 152줄에서 webType === 'category' 조건만 체크
- componentType을 확인하지 않아 select-basic도 가로챔

해결:
- componentType !== 'select-basic' 조건 추가
- select-basic은 카테고리 조건을 건너뛰고 ComponentRegistry로 진행
- 다중선택 등 select-basic의 고급 기능 사용 가능

변경사항:
- DynamicComponentRenderer.tsx 152줄
- 카테고리 조건에 componentType 체크 추가
2025-11-20 18:31:24 +09:00
kjs 9b65c1cbff debug: DynamicComponentRenderer에서 select-basic 조회 로그 추가
문제:
- SelectBasicComponent 렌더링 로그가 전혀 안 보임
- select-basic이 ComponentRegistry에 등록되었는지 확인 필요

디버깅:
- componentType이 'select-basic'일 때 조회 결과 로그
- found: true/false로 등록 여부 확인
- componentConfig 값도 함께 출력

예상 결과:
- found: false면 등록 실패
- found: true면 다른 문제 (렌더링 과정에서 문제)
2025-11-20 18:29:29 +09:00
kjs f765ac4a47 debug: SelectBasicComponent 렌더링 확인용 로그 추가
문제:
- 다중선택 설정했지만 UI에 반영 안됨
- 디버깅 로그가 콘솔에 전혀 안 보임

원인 추정:
- SelectBasicComponent가 아예 렌더링 안되고 있을 가능성
- 또는 다른 select 컴포넌트가 대신 렌더링될 가능성

테스트:
- 최상단에 눈에 띄는 로그 (🚨🚨🚨) 추가
- componentId, componentType, columnName, multiple 값 출력
- 이 로그가 안 보이면 다른 컴포넌트가 렌더링되는 것
2025-11-20 18:26:19 +09:00
kjs f6c96d168b debug: select-basic 다중선택 디버깅 로그 강화
더 명확한 로그 출력:
- 단계별로 구분된 로그
- 각 props 출처별로 명확히 표시
- 최종 isMultiple 값 강조
- 활성화/비활성화 상태 명확히 표시

사용자는 브라우저 콘솔에서 다음을 확인:
1. '🔍 [SelectBasicComponent] ========== 다중선택 디버깅 ==========' 로그 찾기
2. '최종 isMultiple 값' 확인
3. 각 props 출처의 multiple 값 확인
4. / 상태 메시지 확인
2025-11-20 18:23:29 +09:00
kjs 6ea9001a50 fix: select-basic 다중선택 옵션이 실제 화면에 반영되지 않는 문제 해결
문제:
- 설정 패널에서 '다중 선택' 체크했지만 실제 화면에서 작동하지 않음
- componentConfig.multiple이 저장되었지만 컴포넌트에서 인식 못함

원인:
- DynamicComponentRenderer에서 componentConfig를 spread하여 props로 전달
- 하지만 config.multiple만 체크하고 props.multiple를 체크하지 않음

해결:
- isMultiple 변수 추가: props.multiple > config.multiple 우선순위
- 모든 다중선택 로직에서 isMultiple 사용하도록 수정
- 디버깅 로그 추가하여 각 값의 출처 확인

변경사항:
- isMultiple = props.multiple ?? config.multiple ?? false
- 초기화, 업데이트, 렌더링 로직에 isMultiple 적용
- 상세 디버깅 로그로 문제 추적 가능
2025-11-20 18:21:09 +09:00
kjs c57e0218fe feat: select-basic 컴포넌트에 다중선택 기능 추가
기능:
- 설정 패널에 '다중 선택' 체크박스 추가
- multiple 옵션 활성화 시 다중선택 UI 렌더링
- 선택된 항목을 태그 형식으로 표시
- 각 태그에 X 버튼으로 개별 제거 가능
- 드롭다운에 체크박스 표시
- 콤마(,) 구분자로 값 저장/파싱

수정사항:
- SelectBasicConfigPanel: 다중 선택 체크박스 추가
- SelectBasicConfigPanel: config 병합 방식으로 변경 (다른 속성 보호)
- SelectBasicComponent: 초기값 콤마 구분자로 파싱
- SelectBasicComponent: 외부 value 변경 시 다중선택 배열 동기화
- SelectBasicComponent: 다중선택 UI 렌더링 로직 추가

사용법:
1. 설정 패널에서 '다중 선택' 체크
2. 드롭다운에서 여러 항목 선택
3. 선택된 항목이 태그로 표시되며 X로 제거 가능
4. 저장 시 '값1,값2,값3' 형식으로 저장
2025-11-20 18:17:08 +09:00
kjs 3219015a39 fix: 채번규칙 메뉴별 격리 문제 해결
문제: 영업관리 메뉴에서 생성한 채번규칙이 기준정보 메뉴에도 표시됨

원인:
- scope_type='table' 규칙을 조회할 때 menu_objid 필터링 없이 모든 규칙을 포함
- 'OR scope_type = 'table'' 조건이 다른 메뉴의 규칙도 반환

수정:
- scope_type='table' 규칙도 menu_objid로 필터링하도록 변경
- 'OR (scope_type = 'table' AND menu_objid = ANY(cd /Users/kimjuseok/ERP-node && git commit -m "fix: 채번규칙 메뉴별 격리 문제 해결

문제: 영업관리 메뉴에서 생성한 채번규칙이 기준정보 메뉴에도 표시됨

원인:
- scope_type='table' 규칙을 조회할 때 menu_objid 필터링 없이 모든 규칙을 포함
- 'OR scope_type = 'table'' 조건이 다른 메뉴의 규칙도 반환

수정:
- scope_type='table' 규칙도 menu_objid로 필터링하도록 변경
- 'OR (scope_type = 'table' AND menu_objid = ANY($1))' 조건으로 메뉴별 격리
- menu_objid IS NULL인 기존 규칙은 하위 호환성을 위해 유지

영향:
- 각 메뉴에서 생성한 채번규칙은 해당 메뉴(및 형제 메뉴)에서만 표시
- global 규칙은 여전히 모든 메뉴에서 표시
- 기존 데이터는 영향 없음 (menu_objid NULL 조건 유지)"))' 조건으로 메뉴별 격리
- menu_objid IS NULL인 기존 규칙은 하위 호환성을 위해 유지

영향:
- 각 메뉴에서 생성한 채번규칙은 해당 메뉴(및 형제 메뉴)에서만 표시
- global 규칙은 여전히 모든 메뉴에서 표시
- 기존 데이터는 영향 없음 (menu_objid NULL 조건 유지)
2025-11-20 18:05:49 +09:00
kjs 62463e1ca8 fix: 분할 패널 라벨 표시 설정 초기 렌더링 버그 수정
- displayMode가 undefined일 때 기본값 'list' 처리 누락
- 조건문을 (config.rightPanel?.displayMode || 'list') === 'list'로 변경
- 이제 처음 들어갔을 때부터 라벨 표시 설정 UI가 보임

문제: LIST 모드가 기본값인데 초기에는 설정 UI가 안 보이고 테이블 모드로 변경 후 다시 LIST로 바꿔야 보임
원인: undefined === 'list'가 false가 되어 조건문이 작동하지 않음
해결: 기본값 처리 추가
2025-11-20 18:00:30 +09:00
kjs 6e5e3a04f3 fix: 기존 필드의 자동 채우기 테이블 컬럼 초기 로드 추가
- 초기 렌더링 시 기존 필드들의 autoFillFromTable이 설정되어 있으면 컬럼 자동 로드
- useEffect로 localFields 초기화 시점에 모든 필드 순회하며 컬럼 로드
- 사용자가 저장된 설정을 열었을 때 즉시 컬럼 목록 표시

문제: 품목정보 테이블을 선택했지만 컬럼이 표시되지 않음
원인: 기존에 설정된 autoFillFromTable에 대한 컬럼이 초기 로드되지 않음
해결: 초기화 useEffect 추가로 기존 설정 복원
2025-11-20 17:52:40 +09:00
SeongHyun Kim 95b5e3dc7a fix(autocomplete-search-input): 필드 매핑 저장 문제 해결
- types.ts에 targetTable 필드 추가하여 config에 저장되도록 수정
- ConfigPanel에서 targetTable을 localConfig로 관리하여 설정 유지
- Renderer 단순화 (TextInput 패턴 적용)
- Component에서 직접 isInteractive 체크 및 필드 매핑 처리
- ComponentRendererProps 상속으로 필수 props 타입 안정성 확보

문제:
- ConfigPanel 설정이 초기화되는 문제
- 필드 매핑 데이터가 DB에 저장되지 않는 문제

해결:
- 정상 작동하는 TextInput 컴포넌트 패턴 분석 및 적용
- Renderer는 props만 전달, Component가 저장 로직 처리
2025-11-20 17:47:56 +09:00
kjs 86eb9f0425 feat: 자동 채우기 테이블 선택 드롭다운 및 동적 컬럼 로드 추가
- 추가 입력 필드에서 자동 채우기 테이블을 드롭다운으로 선택 가능
- 텍스트 입력 대신 allTables에서 선택하는 방식으로 개선
- 테이블 선택 시 해당 테이블의 컬럼을 자동으로 로드
- autoFillTableColumns 상태로 필드별 테이블 컬럼 관리
- 선택한 테이블에 따라 컬럼 드롭다운이 동적으로 변경됨

사용자 경험 개선:
- 테이블명을 직접 입력하는 대신 목록에서 선택
- 선택한 테이블의 컬럼만 표시되어 혼란 방지
- 원본 테이블(기본) 또는 다른 테이블 선택 가능
2025-11-20 17:44:33 +09:00
kjs 6e92d1855a fix: SelectedItemsDetailInput 설정 패널에서 컬럼 자동 로드 추가
- 원본 테이블(sourceTable) 변경 시 컬럼 자동 로드
- 대상 테이블(targetTable) 변경 시 컬럼 자동 로드
- props로 받은 컬럼은 백업으로 사용하고, 내부에서 로드한 컬럼 우선 사용
- tableManagementApi.getColumnList() 사용하여 동적 로드

이제 원본/대상 테이블 선택 시 해당 테이블의 컬럼 목록이 자동으로 나타남
2025-11-20 17:37:51 +09:00
kjs c51cd7bc87 fix: 컴포넌트 설정 패널 config 병합 및 props 전달 개선
- TableListConfigPanel: handleNestedChange에서 전체 config 병합 로직 추가
- TableListComponent: checkbox.enabled 및 position 기본값 처리 (undefined시 기본값 사용)
- SelectedItemsDetailInputConfigPanel: handleChange에서 전체 config 병합 로직 추가
- SelectedItemsDetailInputConfigPanel: 원본 데이터 테이블 선택 disabled 조건 제거
- UnifiedPropertiesPanel: allTables 로드 및 ConfigPanel에 전달 추가

문제:
1. table-list 컴포넌트 체크박스 설정 변경 시 다른 설정 초기화
2. selected-items-detail-input 설정 변경 시 컴포넌트 이름 등 다른 속성 손실
3. 원본 데이터 테이블 선택이 비활성화되어 있음

해결:
- 모든 설정 패널에서 부분 업데이트 시 기존 config와 병합하도록 수정
- checkbox 관련 기본값 처리로 기존 컴포넌트 호환성 보장
- allTables를 별도로 로드하여 전체 테이블 목록 제공
2025-11-20 17:31:42 +09:00
kjs 6f3bcd7b46 fix: table-list 컴포넌트 컬럼 추가 시 체크박스 등 설정 유지
- UnifiedPropertiesPanel의 handleConfigChange에서 config 병합 로직 추가
- 기존 config와 새 config를 merge하여 checkbox 등 다른 설정이 사라지지 않도록 수정
- 이전에는 부분 업데이트된 config만 전달되어 다른 속성들이 손실되는 문제 해결
2025-11-20 17:18:30 +09:00
kjs d7db8cb07a fix: TableListConfigPanel에 screenTableName 전달 누락 수정
- renderComponentConfigPanel에서 ConfigPanelComponent 호출 시 screenTableName과 tableColumns 전달 추가
- 이전 커밋(e2cc09b2)에서 renderComponentConfigPanel 로직 추가로 인한 회귀 버그 수정
- table-list 컴포넌트 설정 패널에서 컬럼 추가 기능 정상 작동
2025-11-20 17:07:12 +09:00
dohyeons eb6fe57839 dashboard 초기 목록 로딩방식을 csr로 변경 2025-11-20 16:37:52 +09:00
dohyeons 818fd5ac0d same key오류 해결 2025-11-20 16:25:26 +09:00
dohyeons 2facf19429 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-11-20 16:25:17 +09:00
kjs e2cc09b2d6 feat: 검색 필터 위젯 화면별 독립 설정 및 고정 모드 추가
- 검색 필터 설정을 화면별로 독립적으로 저장하도록 개선 (screenId 포함)
- FilterPanel, TableSearchWidget, TableListComponent에 화면 ID 기반 localStorage 키 적용
- 동적 모드(사용자 설정)와 고정 모드(디자이너 설정) 2가지 필터 방식 추가
- 고정 모드에서 컬럼 드롭다운 선택 기능 구현
- 컬럼 선택 시 라벨 및 필터 타입 자동 설정
- ConfigPanel 표시 문제 해결 (type='component' 지원)
- UnifiedPropertiesPanel에서 독립 컴포넌트 ConfigPanel 조회 개선

주요 변경:
- 같은 테이블을 사용하는 다른 화면에서 필터 설정이 독립적으로 관리됨
- 고정 모드에서는 설정 버튼이 숨겨지고 지정된 필터만 표시
- 테이블 정보가 있으면 컬럼을 드롭다운으로 선택 가능
- inputType에 따라 filterType 자동 추론 (number, date, select, text)
2025-11-20 16:21:18 +09:00
kjs 3aee36515a Merge pull request 'feature/screen-management' (#215) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/215
2025-11-20 15:30:20 +09:00
kjs 45ac397417 수주등록 저장기능 2025-11-20 15:30:00 +09:00
kjs b46559ba78 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-20 15:12:32 +09:00
kjs 86313c5e89 fix: SelectedItemsDetailInput 수정 모드에서 null 레코드 삽입 방지
- buttonActions.ts: formData가 배열인 경우 일반 저장 건너뜀
- SelectedItemsDetailInput이 UPSERT를 완료한 후 일반 저장이 실행되어 null 레코드가 삽입되던 문제 해결
- ScreenModal에서 그룹 레코드를 배열로 전달하는 경우 감지하여 처리
- skipDefaultSave 플래그가 제대로 작동하지 않던 문제 근본 해결
2025-11-20 15:07:26 +09:00
dohyeons cdd9bdfd95 차량이 무조건 오른쪽을 보게 수정 2025-11-20 14:47:04 +09:00
dohyeons 751a5da119 Db와 rest api 같이 구현 2025-11-20 14:34:17 +09:00
hyeonsu 6c9ce7a4d9 Merge pull request '대시보드 수정사항 1차 적용' (#214) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/214
2025-11-20 14:03:53 +09:00
dohyeons 461338618e Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-11-20 14:02:34 +09:00
SeongHyun Kim 30cece9bec Merge branch 'ksh' 2025-11-20 13:49:08 +09:00
SeongHyun Kim 68f79db6ed feat(autocomplete-search-input): 필드 자동 매핑 및 저장 위치 선택 기능 추가
- 필드 자동 매핑 기능 구현
  * FieldMapping 타입 추가 (sourceField → targetField)
  * applyFieldMappings() 함수로 선택 시 자동 입력
  * 여러 필드를 한 번에 자동으로 채움 (거래처 선택 → 주소/전화 자동 입력)

- 값 필드 저장 위치 선택 기능 추가
  * ValueFieldStorage 타입 추가 (targetTable, targetColumn)
  * 기본값(화면 연결 테이블) 또는 명시적 테이블/컬럼 지정 가능
  * 중간 테이블, 이력 테이블 등 다중 테이블 저장 지원

- UI/UX 개선
  * 모든 선택 필드를 Combobox 스타일로 통일
  * 각 필드 아래 간략한 사용 설명 추가
  * 저장 위치 동작 미리보기 박스 추가

- 문서 작성
  * 사용_가이드.md 신규 작성 (실전 예제 3개 포함)
  * 빠른 시작 가이드, FAQ, 체크리스트 제공
2025-11-20 13:47:21 +09:00
kjs 703183699f Merge pull request 'feature/screen-management' (#213) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/213
2025-11-20 12:22:20 +09:00
kjs 640351d812 refactor: SelectedItemsDetailInput 하드코딩 제거
- 중복 표시 제거: 품번/품명 하드코딩 삭제 (displayColumns로 이미 표시 중)
- 동적 텍스트: '입력된 품번' → '입력된 항목'으로 일반화
- 이제 어떤 필드 조합이든 동적으로 작동
2025-11-20 12:21:48 +09:00
kjs 348c040e20 refactor: SplitPanelLayout 하드코딩 제거 및 그룹 삭제 기능 구현
- 하드코딩 제거: 필드명 패턴을 동적으로 처리
- 민감한 필드(id, password, token, company_code)만 제외하고 모두 표시
- 그룹 삭제 기능: 중복 제거 활성화 시 관련된 모든 레코드 삭제
- URL 파라미터 초기화: 모달 닫을 때 자동으로 초기화
- 백엔드: deleteGroupRecords API 추가
- 프론트엔드: dataApi.deleteGroupRecords 클라이언트 추가
2025-11-20 12:19:27 +09:00
kjs e3b78309fa 우측 패널 일괄삭제 기능 2025-11-20 11:58:43 +09:00
kjs c3f58feef7 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-20 10:27:38 +09:00
kjs 34cd7ba9e3 feat: 수정 모드 UPSERT 기능 구현
- SelectedItemsDetailInput 컴포넌트 수정 모드 지원
- 그룹화된 데이터 UPSERT API 추가 (/api/data/upsert-grouped)
- 부모 키 기준으로 기존 레코드 조회 후 INSERT/UPDATE/DELETE
- 각 레코드의 모든 필드 조합을 고유 키로 사용
- created_date 보존 (UPDATE 시)
- 수정 모드에서 groupByColumns 기준으로 관련 레코드 조회
- 날짜 타입 ISO 형식 자동 감지 및 포맷팅 (YYYY.MM.DD)

주요 변경사항:
- backend: dataService.upsertGroupedRecords() 메서드 구현
- backend: dataRoutes POST /api/data/upsert-grouped 엔드포인트 추가
- frontend: ScreenModal에서 groupByColumns 파라미터 전달
- frontend: SelectedItemsDetailInput 수정 모드 로직 추가
- frontend: 날짜 필드 타임존 제거 및 포맷팅 개선
2025-11-20 10:23:54 +09:00
SeongHyun Kim 6d0acdd1ec fix: ModalRepeaterTable reference 매핑 처리 순서 및 API 파라미터 수정
문제:
- reference 매핑 시 조인 조건의 소스 필드 값이 undefined
- API 호출 시 filters 파라미터를 백엔드가 인식 못함

해결:
- 컬럼 처리를 2단계로 분리 (source/manual → reference)
- API 파라미터 변경 (filters→search, limit/offset→size/page)
- 응답 경로 수정 (data.data → data.data.data)

결과:
- 외부 테이블 참조 매핑 정상 작동
- 품목 선택 시 customer_item_mapping에서 단가 자동 조회 성공
2025-11-20 10:16:49 +09:00
dohyeons 33350a4d46 feat: Digital Twin Editor 테이블 매핑 UI 및 백엔드 API 구현 2025-11-20 10:15:58 +09:00
SeongHyun Kim d5d267e63a feat(modal-repeater-table): 컬럼 매핑 및 계산 규칙 UI 대폭 개선
새로운 기능
- 컬럼별 독립적인 소스 테이블 선택 기능
- SourceColumnSelector, ReferenceColumnSelector 컴포넌트 추가
- 계산 규칙 자동 동기화 로직 (cleanupInitialConfig)

UI/UX 개선
- 컬럼 설정 UI를 세로 레이아웃으로 재구성 (h-10 통일)
- 매핑 타입별 색상 구분 (파란색/보라색/초록색)
- 계산 규칙 섹션 재디자인 (안내 박스, 번호 배지, 빈 상태)
- 현재 설정 시각화 (코드 스타일 표시)

버그 수정
- 계산 규칙 삭제 시 컬럼이 수정 불가능 상태로 남는 문제 해결
- 결과 필드 변경 시 이전 필드의 calculated 속성 제거
- 초기 로드 시 계산 규칙과 컬럼 속성 동기화

개선 사항
- 모든 입력 필드의 높이와 텍스트 크기 일관성 확보
- 섹션별 명확한 제목과 설명 추가
- 접근성 향상 (ARIA 레이블, 포커스 스타일)
2025-11-19 17:09:12 +09:00
kjs d4895c363c refactor: 디버깅 로그 제거 및 코드 정리
변경사항:
- handleBatchSave의 모든 console.log 제거
- 핵심 로직만 유지 (데이터 매핑, 조합 생성, 저장)
- 코드 가독성 향상

제거된 로그:
- modalDataStore 데이터 확인 로그
- parentDataMapping 설정 로그
- 품목/그룹 처리 로그
- 조합 생성/병합 로그
- 데이터 소스 상세 로그
- 저장 요청/결과 로그

유지된 기능:
- Zustand modalDataStore에서 부모 데이터 가져오기
- 무한 깊이 모달 지원
- 완전히 설정 기반 parentDataMapping
- 카티션 곱 조합 생성
- 하드코딩 없는 동적 매핑
2025-11-19 13:57:54 +09:00
kjs 762ab8e684 fix: Zustand modalDataStore에서 부모 데이터 가져오기
문제:
- modalDataStore가 window 전역 변수가 아닌 Zustand store임
- window.__modalDataRegistry로 접근 시도했으나 빈 객체 반환
- 거래처 데이터를 찾을 수 없어 customer_code 매핑 실패

해결:
- useModalDataStore.getState().dataRegistry로 Zustand store 직접 접근
- ModalDataItem[] 배열에서 originalData 추출
- 각 테이블별 데이터를 modalDataStore 객체로 변환
- 거래처(customer_mng), 품목(item_info) 데이터 모두 접근 가능

기술적 변경:
- dynamic import로 Zustand store 로드
- ModalDataItem 구조 이해 및 originalData 추출
- 에러 핸들링 (store 로드 실패 시)
- 상세한 디버깅 로그 (테이블별 데이터 count)
2025-11-19 13:51:24 +09:00
kjs 97b5cd7a5b fix: 다단계 모달 환경에서 부모 데이터 매핑 수정
문제:
- 메인 화면(거래처 선택) → 첫 번째 모달(품목 선택) → 두 번째 모달(상세 입력)
- selectedRowsData는 바로 이전 화면 데이터만 제공하여 2단계 이전 데이터 접근 불가
- customer_id가 NULL로 저장됨

해결:
- modalDataStore의 전역 레지스트리에서 모든 누적 데이터 접근
- sourceTable에 따라 적절한 데이터 소스 자동 선택
- 거래처 데이터(customer_mng)를 modalDataStore에서 직접 가져옴

기술적 변경:
- ButtonPrimaryComponent: allComponents에서 componentConfigs 수집 및 전달
- ButtonActionContext: componentConfigs 속성 추가
- handleBatchSave: modalDataStore에서 테이블별 데이터 조회
- parentDataMapping 로직: sourceTable 기반 데이터 소스 자동 감지
- 디버깅 로그 강화 (modalDataStore 키, 데이터 소스 추적)
2025-11-19 13:48:44 +09:00
kjs f4e4ee13e2 feat: 부모 데이터 매핑 기능 구현 (선택항목 상세입력 컴포넌트)
- 여러 테이블(거래처, 품목 등)에서 데이터를 가져와 자동 매핑 가능
- 각 매핑마다 소스 테이블, 원본 필드, 저장 필드를 독립적으로 설정
- 검색 가능한 Combobox로 테이블 및 컬럼 선택 UX 개선
- 소스 테이블 선택 시 해당 테이블의 컬럼 자동 로드
- 라벨, 컬럼명, 데이터 타입으로 검색 가능
- 세로 레이아웃으로 가독성 향상

기술적 변경사항:
- ParentDataMapping 인터페이스 추가 (sourceTable, sourceField, targetField)
- buttonActions.ts의 handleBatchSave에서 소스 테이블 기반 데이터 소스 자동 판단
- tableManagementApi.getColumnList() 사용하여 테이블 컬럼 동적 로드
- Command + Popover 조합으로 검색 가능한 Select 구현
- 각 매핑별 독립적인 컬럼 상태 관리 (mappingSourceColumns)
2025-11-19 13:22:49 +09:00
dohyeons eeed671436 3d - 배치 구현 2025-11-19 12:00:55 +09:00
SeongHyun Kim 8eccdd0b4c fix: 수주등록 항목추가 시 정보출력 가능하게 수정
문제:
- 조건부 컨테이너 내부의 modal-repeater-table 컴포넌트가 데이터 업데이트 불가
- ConditionalSectionViewer가 RealtimePreview에 formData/onFormDataChange 미전달

해결:
- ConditionalSectionViewer.tsx: RealtimePreview에 formData, onFormDataChange props 추가
- DynamicComponentRenderer.tsx: 디버깅 로그 정리
- ScreenModal.tsx: 디버깅 로그 정리

영향:
- 수주 등록 화면 품목 추가 기능 정상 작동
- 조건부 컨테이너 내부 모든 폼 컴포넌트 데이터 바인딩 정상화

Refs: #수주관리 #modal-repeater-table #ConditionalContainer
2025-11-19 11:48:00 +09:00
kjs b74cb94191 화면 복사기능 수정 2025-11-19 10:03:38 +09:00
SeongHyun Kim bfc86fbcfa fix: ResizableDialog 닫기 버튼 클릭 불가 문제 해결 2025-11-19 09:12:17 +09:00
SeongHyun Kim 0bedd8bc0b fix: Dialog 모달 내부 input 필드 텍스트 입력 불가 문제 해결
- ResizableDialog 콘텐츠 영역에 pointer-events 및 z-index 설정 추가
- TextInputComponent를 제어 컴포넌트에서 비제어 컴포넌트로 변경 (value → defaultValue)
- ItemSelectionModal 및 TextInputComponent 디버그 로그 제거

수정 파일:
- frontend/components/ui/resizable-dialog.tsx
- frontend/lib/registry/components/text-input/TextInputComponent.tsx
- frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx
2025-11-18 18:40:25 +09:00
dohyeons cec631d0f7 야드관리 3d 에러 및 기본 설정 수정 2025-11-18 17:57:19 +09:00
dohyeons 1acbd76eb8 아이콘 진행방향에 따른 회전 재구현 2025-11-18 17:46:32 +09:00
SeongHyun Kim 9c8ec879d9 Merge branch 'ksh' 2025-11-18 16:20:38 +09:00
SeongHyun Kim b844953fe0 fix: ItemSelectionModal 선택 로직 및 디버깅 개선
- uniqueField 값이 undefined일 때 객체 참조 비교로 폴백
- 멀티셀렉트 모드에서 선택/해제 로직 안정화
- 체크박스 클릭 이벤트 전파 개선
- 유효한 컬럼만 렌더링하도록 필터링 추가
- 디버깅을 위한 콘솔 로그 추가
- 선택된 항목의 uniqueField 값 표시
2025-11-18 16:19:56 +09:00
kjs ade71313b4 Merge pull request 'feature/screen-management' (#212) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/212
2025-11-18 16:16:23 +09:00
kjs 5f026e88ab Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-18 16:15:57 +09:00
kjs e1a5befdf7 feat: 기간별 단가 설정 기능 구현 - 자동 계산 시스템
- 선택항목 상세입력 컴포넌트 확장
  - 실시간 가격 계산 기능 추가 (할인율/할인금액, 반올림 방식)
  - 카테고리 값 기반 연산 매핑 시스템
  - 3단계 드릴다운 방식 설정 UI (메뉴 → 카테고리 → 값 매핑)

- 설정 가능한 계산 로직
  - autoCalculation 설정으로 계산 필드명 동적 지정
  - valueMapping으로 카테고리 코드와 연산 타입 매핑
  - 할인 방식: none/rate/amount
  - 반올림 방식: none/round/floor/ceil
  - 반올림 단위: 1/10/100/1000

- UI 개선
  - 입력 필드 가로 배치 (반응형 Grid)
  - 카테고리 타입 필드 옵션 로딩 개선
  - 계산 결과 필드 자동 표시 및 읽기 전용 처리
  - 날짜 입력 필드 네이티브 피커 지원

- API 연동
  - 2레벨 메뉴 목록 조회
  - 메뉴별 카테고리 컬럼 조회
  - 카테고리별 값 목록 조회

- 문서화
  - 기간별 단가 설정 가이드 작성
2025-11-18 16:12:47 +09:00
SeongHyun Kim 8272361063 chore: Section Card/Paper 컴포넌트 디버깅 로그 제거
목적:
- 콘솔창이 너무 많은 디버깅 정보로 지저분해지는 문제 해결
- 정상 작동 시 불필요한 로그 출력 최소화

변경사항:
- UnifiedPropertiesPanel: 4개 디버깅 로그 제거
  • renderDetailTab 컴포넌트 타입 확인 로그
  • DataTable/Component 타입 감지 로그
  • DynamicComponentConfigPanel onChange 로그
- RealtimePreviewDynamic: baseStyle 크기 정보 로그 주석 처리

결과:
- Section Card/Paper 사용 시 깔끔한 콘솔
- 에러 발생 시에만 에러 메시지 표시 (기존 핸들링 유지)
- 필요시 주석 해제로 디버깅 로그 재활성화 가능
2025-11-18 13:12:50 +09:00
SeongHyun Kim 1a82c8ea94 fix: ScreenModal에 TableOptionsProvider 추가하여 TableSearchWidget 에러 해결
문제:
- 거래처별 품목 정보 등 모달 화면에서 TableSearchWidget 사용 시 에러 발생
- Error: useTableOptions must be used within TableOptionsProvider

원인:
- ScreenModal에서 화면을 렌더링할 때 필수 Context Provider 누락
- TableSearchWidget은 TableOptionsContext를 필수로 사용하는데 모달 환경에서 제공되지 않음

해결:
- ScreenModal에 TableOptionsProvider와 TableSearchWidgetHeightProvider 추가
- 모달 내부 화면 컴포넌트들이 정상적으로 Context를 사용할 수 있도록 수정

영향:
- 거래처별 품목 정보 화면의 '품목 추가' 버튼 정상 작동
- 모든 모달 화면에서 TableSearchWidget 사용 가능
- 기존 화면 페이지(/screens/[screenId])는 이미 Provider가 있어 영향 없음
2025-11-18 11:50:13 +09:00
SeongHyun Kim 108af2a68b feat: Section Card/Paper 레이아웃 컴포넌트 추가 및 설정 패널 통합
새로운 그룹화 레이아웃 컴포넌트 2종 추가:
- Section Card: 제목+테두리 기반 명확한 섹션 구분
- Section Paper: 배경색 기반 미니멀한 섹션 구분

주요 변경사항:
- 새 컴포넌트 등록 (각 4개 파일: Component, ConfigPanel, Renderer, index)
- UnifiedPropertiesPanel에 인라인 설정 UI 추가 (280줄)
- DetailSettingsPanel ConfigPanel 인터페이스 통일화 (config → componentConfig)
- getComponentConfigPanel에 동적 import 매핑 추가
- 기존 컴포넌트 타입 정리 (autocomplete, entity-search, modal-repeater)

특징:
- shadcn/ui 기반 일관된 디자인 시스템 준수
- 중첩 박스 금지 원칙 적용
- 반응형 지원 (모바일 우선)
- collapsible 기능 지원 (Section Card)
2025-11-18 11:28:22 +09:00
kjs 967b76591b 플레이스홀더 변경 2025-11-18 11:08:05 +09:00
kjs 4cd27639e6 fix: Collapsible 레이아웃 구조 수정
- CardContent 내부에서 CollapsibleContent가 제대로 렌더링되도록 구조 수정
- mb-2, mt-2로 간격 조정
- 오른쪽으로 밀려나는 문제 해결
2025-11-18 10:30:04 +09:00
kjs 7bb26e0e30 fix: Collapsible 변수 참조 오류 수정
- isGroupExpanded 변수를 올바르게 사용하도록 수정
- 불필요한 주석 제거
- map 함수에 return 문 추가
2025-11-18 10:28:07 +09:00
kjs def94c41f4 refactor: 설정 패널 UI 개선 - Collapsible로 정리
- 필드 그룹을 Collapsible로 변경하여 펼침/접힘 가능
- 항목 표시 설정도 Collapsible로 분리하여 깔끔하게 정리
- 그룹 제목에 displayItems 개수 표시
- 기본적으로 그룹은 펼쳐진 상태, 표시 설정은 접힌 상태
- ChevronDown/ChevronRight 아이콘으로 펼침 상태 표시
- 복잡한 설정을 단계적으로 볼 수 있어 가독성 대폭 향상
2025-11-18 10:25:28 +09:00
kjs eef1451c5a fix: 항목 표시 설정을 그룹별로 분리
- FieldGroup에 displayItems 추가 (그룹별 독립적인 표시 설정)
- SelectedItemsDetailInputConfig에서 전역 displayItems 제거
- renderDisplayItems에 groupId 파라미터 추가하여 그룹별 설정 사용
- 설정 패널에서 그룹별로 displayItems 관리
- 각 그룹마다 다른 표시 형식 가능 (예: 거래처 정보 vs 단가 정보)
- 그룹의 필드만 선택 가능하도록 필터링
2025-11-18 10:21:36 +09:00
kjs e234f88577 feat: 항목 표시 설정 기능 추가 (기본값, 빈 값 처리 포함)
- DisplayItem 타입 추가 (icon, field, text, badge)
- 필드별 표시 형식 지원 (text, currency, number, date, badge)
- 빈 값 처리 옵션 추가 (hide, default, blank)
- 기본값 설정 기능 추가
- 스타일 옵션 추가 (굵게, 밑줄, 기울임, 색상)
- renderDisplayItems 헬퍼 함수로 유연한 표시 렌더링
- SelectedItemsDetailInputConfigPanel에 항목 표시 설정 UI 추가
- displayItems가 없으면 기존 방식(모든 필드 나열)으로 폴백
2025-11-18 10:14:31 +09:00
kjs cddce40f35 fix: 첫 글자 입력 시 포커스 유지 문제 해결
- handleAddGroupEntry: + 추가 버튼 클릭 시 미리 빈 entry를 배열에 추가
- handleFieldChange: 기존 entry 업데이트만 수행 (새로운 entry 추가 로직 제거)
- 이제 첫 글자 입력 시에도 배열 길이가 변하지 않아 포커스가 유지됨
2025-11-18 10:02:27 +09:00
kjs 3d74b9deb2 fix: 선택항목 상세입력 컴포넌트 입력 시 포커스 유지 개선
- 입력 중 onFormDataChange 호출 제거하여 불필요한 리렌더링 방지
- 저장 버튼 클릭 시에만 데이터 전달하도록 변경 (beforeFormSave 이벤트)
- handleSave에서 beforeFormSave 이벤트 발생 및 100ms 대기
- 원본 데이터 표시 버그 수정 (modalData 중첩 구조 처리)
- fieldGroups 구조 감지 로직 수정 (details → fieldGroups)

이제 사용자가 타이핑할 때 포커스가 유지됩니다.
2025-11-18 10:00:56 +09:00
kjs e9268b3f00 feat: 선택항목 상세입력 컴포넌트 그룹별 독립 입력 구조로 개선
- 데이터 구조 변경: ItemData.details → ItemData.fieldGroups (그룹별 관리)
- 각 필드 그룹마다 독립적으로 여러 항목 추가/수정/삭제 가능
- renderFieldsByGroup: 그룹별 입력 항목 목록 + 편집 + 추가 버튼 구현
- renderGridLayout/renderCardLayout: 품목별 그룹 카드 표시로 단순화
- handleFieldChange: groupId 파라미터 추가 (itemId, groupId, entryId, fieldName, value)
- handleAddGroupEntry, handleRemoveGroupEntry, handleEditGroupEntry 핸들러 추가
- buttonActions handleBatchSave: fieldGroups 구조 처리하도록 수정
- 원본 데이터 표시 버그 수정: modalData의 중첩 구조 처리

사용 예:
- 품목 1
  - 그룹 1 (거래처 정보): 3개 항목 입력 가능
  - 그룹 2 (단가 정보): 5개 항목 입력 가능
- 각 항목 클릭 → 수정 가능
- 저장 시 모든 입력 항목이 개별 레코드로 저장됨
2025-11-18 09:56:49 +09:00
SeongHyun Kim b09bd64083 Merge branch 'main' into ksh 2025-11-17 17:12:03 +09:00
SeongHyun Kim 23cd677413 fix: 컴포넌트 등록 및 entity-search 검증 개선
- 컴포넌트 등록을 useEffect → 즉시 실행으로 변경 (3개 컴포넌트)
- entity-search-input tableName 검증 추가 (FE/BE)
- DynamicComponentRenderer에서 componentConfig props 전달 수정
- EntitySearchModal key prop 경고 해결
- 불필요한 ScreenDesigner 렌더링 코드 제거

Fixes: 컴포넌트 미등록 에러, tableName undefined 500 에러, React key 경고
2025-11-17 16:48:42 +09:00
dohyeons 5142a1254e 통계 카드 헤더 제거해도 삭제 버튼은 남기기 2025-11-17 16:37:22 +09:00
dohyeons 8006255bbf 디버깅용 console.log 삭제 2025-11-17 16:32:50 +09:00
SeongHyun Kim aee4e86036 entity-search-input tableName 유효성 검증 추가 및 에러 메시지 개선 2025-11-17 16:18:38 +09:00
kjs 6839deac97 Merge pull request 'feature/screen-management' (#211) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/211
2025-11-17 16:00:05 +09:00
kjs 84f0a66155 Merge branch 'main' into feature/screen-management 2025-11-17 15:59:54 +09:00
kjs 9d68268910 Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-17 15:59:43 +09:00
kjs 289677a971 fix: SelectedItemsDetailInput Select 빈 값 에러 수정 및 inputType 디버깅 로그 추가
- Select 컴포넌트에서 빈 문자열 value를 가진 SelectItem 제거
- category/code 타입 필드의 옵션 로딩 디버깅 로그 추가
- 빈 값 필터링으로 'SelectItem must not have empty value' 에러 해결
- codeCategory 자동 감지 로직 디버깅 강화
2025-11-17 15:59:25 +09:00
kjs cdae78b125 Merge pull request 'feature/screen-management' (#210) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/210
2025-11-17 15:25:33 +09:00
kjs d218fd7a1a Merge branch 'main' into feature/screen-management 2025-11-17 15:25:26 +09:00
kjs 5bf3c0fcd7 Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-17 15:25:10 +09:00
kjs bc557c4074 상세입력 컴포넌트 테이블 선택 기능 추가 2025-11-17 15:25:08 +09:00
dohyeons fb9bc268a0 맵 위젯 에러 해결 2025-11-17 15:09:50 +09:00
dohyeons 542f2ccc96 실시간 지도 마커 업데이트 구현 및 마커 종류에 트럭 추가 2025-11-17 15:05:59 +09:00
SeongHyun Kim 12000ca059 feat: 카테고리 삭제 시 실제 데이터 사용 여부 확인 및 차단 기능
- 카테고리 값/컬럼이 실제 데이터에서 사용 중이면 삭제 차단
- 사용 중인 데이터 개수 및 메뉴 목록 표시
- 물리적 삭제 방식으로 변경
- 상세 에러 메시지 팝업 추가
2025-11-17 14:22:50 +09:00
SeongHyun Kim 91f502c14b Merge branch 'ksh' 2025-11-17 13:37:33 +09:00
SeongHyun Kim e9c64f65c8 카테고리 값 삭제 로직 개선
- 소프트 삭제(is_active=false)에서 하드 삭제(DELETE)로 변경
- 하위 카테고리 체크 시 is_active 조건 제거하여 정확성 향상
- 불필요한 updated_at, updated_by 파라미터 제거
2025-11-17 13:36:46 +09:00
kjs f4efdd0e0b Merge pull request '컴포넌트 렌더링 오류 수정' (#209) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/209
2025-11-17 13:32:10 +09:00
kjs be63452834 Merge branch 'main' into feature/screen-management 2025-11-17 13:31:57 +09:00
kjs a783317820 컴포넌트 렌더링 오류 수정 2025-11-17 13:31:09 +09:00
kjs d420d1dd40 Merge pull request 'feature/screen-management' (#208) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/208
2025-11-17 13:10:25 +09:00
kjs d1d76bbea8 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-17 13:08:16 +09:00
kjs a6e6a14fd1 선택항목 상게입력 컴포넌트 구현 2025-11-17 12:23:45 +09:00
dohyeons 227ab1904c 위젯 헤더 설정 디자이너 화면에서도 적용되도록 2025-11-17 12:22:13 +09:00
dohyeons 800da3bf21 list위젯 카드 기능 재구현 2025-11-17 12:16:55 +09:00
dohyeons 7ba05da288 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-11-17 12:12:11 +09:00
dohyeons 251c4e3a66 팝업 꼬리 제거 2025-11-17 12:12:02 +09:00
dohyeons 3e9c566834 지도 위젯 팝업 수정 및 폴리곤 매핑 추가 2025-11-17 12:10:29 +09:00
kjs 2c099feea0 조건부 컨테이너 2025-11-17 10:09:02 +09:00
SeongHyun Kim 660f81edbc fix: table-list 컴포넌트 크기 조절 및 동기화 문제 해결
- 기본 너비 120px → 1000px 변경
- 선택 영역과 컴포넌트 영역 크기 동기화
- 편집 패널에서 너비/높이 조절 시 즉시 반영되도록 개선
2025-11-17 10:01:09 +09:00
dohyeons ff23aa7d1d 대시보드 제목 누르면 편집화면으로 이동 구현 2025-11-17 09:49:13 +09:00
kjs d1ce14de7a Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-17 09:32:12 +09:00
kjs 83008886f0 Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-14 18:26:20 +09:00
kjs 226cb6a266 모달에서 수정 2025-11-14 18:26:17 +09:00
kjs dd77ddc141 fix: 조건부 컨테이너에서 화면이 렌더링되지 않는 문제 수정
- InteractiveScreenViewer는 screenId가 아닌 component, allComponents를 받음
- screenApi.getLayout()과 getScreen()으로 화면 데이터 로드
- 로드된 컴포넌트들을 InteractiveScreenViewer로 렌더링
- 화면 로딩 상태 추가
- screenInfo 전달하여 테이블 컨텍스트 제공
2025-11-14 18:20:52 +09:00
kjs 2ec6e3e920 fix: Select 컴포넌트 빈 문자열 값 오류 수정
- Radix UI Select는 빈 문자열 value를 허용하지 않음
- "선택 안 함" 옵션의 값을 "" → "none"으로 변경
- onValueChange에서 "none" 체크하여 screenId를 null로 설정
2025-11-14 18:13:28 +09:00
kjs 7d1ecf718b fix: 조건부 컨테이너 API 임포트 경로 수정
- screenManagementApi → screenApi로 변경
- @/lib/api/screenManagement → @/lib/api/screen
- screenApi.getScreens() 사용하여 화면 목록 조회
- ScreenDefinition 타입에 맞게 필드명 수정 (id → screenId)
2025-11-14 18:00:56 +09:00
SeongHyun Kim e2a4df575c Merge branch 'ksh' into main - 거래처별 품목정보 컴포넌트 추가 2025-11-14 17:41:03 +09:00
kjs f5756e184f feat: 조건부 컨테이너를 화면 선택 방식으로 개선
- ConditionalSection 타입 변경 (components[] → screenId, screenName)
  * 각 조건마다 컴포넌트를 직접 배치하는 대신 기존 화면을 선택
  * 복잡한 입력 폼도 화면 재사용으로 간단히 구성

- ConditionalSectionDropZone을 ConditionalSectionViewer로 교체
  * 드롭존 대신 InteractiveScreenViewer 사용
  * 선택된 화면을 조건별로 렌더링
  * 디자인 모드에서 화면 미선택 시 안내 메시지 표시

- ConfigPanel에서 화면 선택 드롭다운 구현
  * screenManagementApi.getScreenList()로 화면 목록 로드
  * 각 섹션마다 화면 선택 Select 컴포넌트
  * 선택된 화면의 ID와 이름 자동 저장 및 표시
  * 로딩 상태 표시

- 기본 설정 업데이트
  * defaultConfig에서 components 제거, screenId 추가
  * 모든 섹션 기본값을 screenId: null로 설정

- README 문서 개선
  * 화면 선택 방식으로 사용법 업데이트
  * 사용 사례에 화면 ID 예시 추가
  * 구조 다이어그램 수정 (드롭존 → 화면 표시)
  * 디자인/실행 모드 설명 업데이트

장점:
- 기존 화면 재사용으로 생산성 향상
- 복잡한 입력 폼도 간단히 조건부 표시
- 화면 수정 시 자동 반영
- 유지보수 용이
2025-11-14 17:40:07 +09:00
dohyeons 385ecdc46a 통계카드에 시간 표시 2025-11-14 16:55:52 +09:00
SeongHyun Kim 4fa57d67d6 거래처별 품목정보 검색바 및 카테고박스 추가 2025-11-14 16:49:49 +09:00
SeongHyun Kim 361cb56a1d 거래처관리-품목등록 화면 컴포넌트 제작 2025-11-14 16:30:38 +09:00
kjs e6949bdd67 fix: 수주등록 모달 및 품목 검색 UI 개선
- 품목 검색 모달에서 컬럼명 대신 라벨명 표시
  * ItemSelectionModal에 columnLabels prop 추가
  * ModalRepeaterTableComponent에서 columns 설정의 라벨 매핑 생성
  * 테이블 헤더에 한글 라벨 표시 (품번, 품명, 규격, 재질 등)

- 이미 추가된 품목은 검색 결과에서 완전 제외
  * filteredResults로 중복 항목 필터링
  * 회색 표시 대신 목록에서 아예 제거
  * 사용자 친화적인 안내 메시지 추가

- 수주등록 버튼 크기 및 렌더링 수정
  * 기본 크기를 200x40에서 120x40으로 변경 (다른 버튼과 통일)
  * h-full w-full 클래스 적용하여 컨테이너 크기에 맞게 렌더링
  * style prop의 width/height 제거하여 Tailwind 클래스 우선순위 문제 해결

- 수주등록 모달에 판매 유형 및 무역 정보 추가
  * 국내/해외 판매 선택 기능
  * 해외 판매 시 무역 정보 섹션 표시 (인코텀즈, 결제조건, 통화 등)
  * 거래처 정보 확장 (담당자, 납품처, 납품장소)

- 품목 반복 테이블 컬럼 조정
  * 품목번호를 품번으로 변경
  * 비고 컬럼 제거
  * 규격, 재질 컬럼 추가
  * 납품일을 납기일로 변경
2025-11-14 16:19:27 +09:00
kjs 64e6fd1920 feat: 수주등록 모달 및 범용 컴포넌트 개발
- 범용 컴포넌트 3종 개발 및 레지스트리 등록:
  * AutocompleteSearchInput: 자동완성 검색 입력 컴포넌트
  * EntitySearchInput: 엔티티 검색 모달 컴포넌트
  * ModalRepeaterTable: 모달 기반 반복 테이블 컴포넌트

- 수주등록 전용 컴포넌트:
  * OrderCustomerSearch: 거래처 검색 (AutocompleteSearchInput 래퍼)
  * OrderItemRepeaterTable: 품목 관리 (ModalRepeaterTable 래퍼)
  * OrderRegistrationModal: 수주등록 메인 모달

- 백엔드 API:
  * Entity 검색 API (멀티테넌시 지원)
  * 수주 등록 API (자동 채번)

- 화면 편집기 통합:
  * 컴포넌트 레지스트리에 등록
  * ConfigPanel을 통한 설정 기능
  * 드래그앤드롭으로 배치 가능

- 개발 문서:
  * 수주등록_화면_개발_계획서.md (상세 설계 문서)
2025-11-14 14:43:53 +09:00
dohyeons 5533a134c6 리스크 위젯 날짜 형식 통일 2025-11-14 13:47:55 +09:00
hyeonsu 0c1292c55b Merge pull request '대시보드 수정사항 적용' (#207) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/207
2025-11-14 12:11:07 +09:00
dohyeons 02d4a3a3d3 리스크 알림 위젯 날짜 포맷 변경 2025-11-14 12:10:10 +09:00
dohyeons 05273daa92 커스텀 통계 카드 위젯을 수정 2025-11-14 11:35:16 +09:00
dohyeons 2eb8c3a61b 리스트 카드 렌더링 문제 해결 2025-11-14 11:16:03 +09:00
dohyeons a491f08337 지도 위젯에서도 마커 종류 선택 가능하게 구현 2025-11-14 10:49:11 +09:00
dohyeons a3503c0b9f 지도 위젯 별 polling 설정 구현 2025-11-14 10:26:09 +09:00
dohyeons b3e217c1de 빌드 에러 해결 2025-11-13 18:09:54 +09:00
dohyeons 50410475c0 배포 오류 해결 2025-11-13 18:06:11 +09:00
kjs 075869c89c fix: 분할 패널 카테고리 API 엔드포인트 경로 수정
- 잘못된 API 경로 수정
  - 이전: /api/table-type/category-values/{tableName}/{columnName}
  - 수정: /api/table-categories/{tableName}/{columnName}/values
- 백엔드 라우트와 일치하도록 변경 (app.ts에서 /api/table-categories로 마운트됨)
- 좌측/우측 패널 카테고리 매핑 로드 API 호출 경로 수정
2025-11-13 17:55:10 +09:00
kjs 702b506665 feat: 분할 패널 테이블에서 카테고리 값을 라벨로 표시하는 기능 추가
- 좌측/우측 패널 테이블의 카테고리 타입 컬럼 매핑 로드
- formatCellValue 함수 추가하여 카테고리 코드를 라벨+배지로 변환
- 좌측 패널 테이블 렌더링 부분(그룹화/일반)에 formatCellValue 적용
- 우측 패널 테이블 렌더링 부분에 formatCellValue 적용
- Badge 컴포넌트 import 및 카테고리별 색상 표시

변경 사항:
- 카테고리 매핑 상태 추가 (leftCategoryMappings, rightCategoryMappings)
- API 클라이언트 import 추가
- 카테고리 값 조회 API 호출 useEffect 2개 추가
- 셀 값 포맷팅 함수 formatCellValue 추가
- 모든 테이블 td 렌더링에서 String(value) → formatCellValue() 적용
2025-11-13 17:52:33 +09:00
kjs 2a52f25c10 feat: 모달 저장 후 부모 화면 테이블 자동 새로고침 기능 추가
- ScreenModal에 onRefresh 콜백 추가하여 refreshTable 이벤트 발송
- InteractiveScreenViewerDynamic에 onRefresh, onFlowRefresh prop 추가 및 하위 컴포넌트로 전달
- TableListComponent에 refreshTable 이벤트 리스너 추가
- SplitPanelLayoutComponent에 refreshTable 이벤트 리스너 추가하여 좌/우측 패널 모두 새로고침
- 모달에서 데이터 저장 시 부모 화면의 모든 테이블 컴포넌트가 자동으로 새로고침되도록 개선

변경된 파일:
- frontend/components/common/ScreenModal.tsx
- frontend/components/screen/InteractiveScreenViewerDynamic.tsx
- frontend/lib/registry/components/table-list/TableListComponent.tsx
- frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx
2025-11-13 17:42:20 +09:00
kjs e21ec4c7b7 Merge pull request 'feature/screen-management' (#206) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/206
2025-11-13 17:07:00 +09:00
kjs c4ea39bf83 Merge branch 'main' into feature/screen-management 2025-11-13 17:06:54 +09:00
kjs ab1cbd37b3 Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-13 17:06:42 +09:00
kjs 296ee3e825 테이블 데이터 필터링 기능 및 textarea컴포넌트 자동 매핑 삭제 2025-11-13 17:06:41 +09:00
kjs 76167ab424 Merge pull request 'feature/screen-management' (#205) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/205
2025-11-13 15:28:32 +09:00
kjs 2663400e26 Merge branch 'main' into feature/screen-management 2025-11-13 15:28:25 +09:00
kjs a828f54663 feat: 카테고리 배지 없음 옵션 추가
- 카테고리 색상 설정 시 '배지 없음' 옵션 추가
- color='none'일 때 배지 대신 일반 텍스트로 표시
- CategoryValueEditDialog, CategoryValueAddDialog에 배지 없음 버튼 추가
- InteractiveDataTable, TableListComponent에서 배지 없음 처리
- CategoryValueManager에서 배지 없음 표시 추가
- 기본 색상을 배지 없음으로 변경
2025-11-13 15:24:31 +09:00
kjs f73f788b0a fix: 카테고리 컬럼 중복 데이터 문제 해결
- table_column_category_values JOIN 시 회사 데이터만 사용하도록 수정
- 공통 데이터(company_code='*') 사용 금지
- is_active=true 필터 추가로 활성화된 카테고리만 조회
- entityJoinService.ts의 buildJoinQuery 및 buildCountQuery 수정
- 품목 정보 테이블 등에서 발생하던 2배 중복 문제 해결
2025-11-13 15:16:36 +09:00
kjs 36bff64145 feat: 카테고리 컬럼 메뉴별 매핑 기능 구현
- category_column_mapping 테이블 생성 (마이그레이션 054)
- 테이블 타입 관리에서 2레벨 메뉴 선택 기능 추가
- 카테고리 컬럼 조회 시 현재 메뉴 및 상위 메뉴 매핑 자동 조회
- 캐시 무효화 로직 개선
- 메뉴별 카테고리 설정 저장 및 불러오기 기능 구현
2025-11-13 14:41:24 +09:00
kjs 9dc8a51f4c fix: 메뉴 URL 제거 시 화면 할당도 함께 해제
## 문제점
- menu_url을 빈 값으로 저장해도 screen_menu_assignments 테이블의
  화면 할당(is_active='Y')이 남아있어 메뉴 클릭 시 여전히 화면이 열림
- AppLayout의 handleMenuClick이 우선적으로 screen_menu_assignments를
  조회하므로 menu_url보다 화면 할당이 우선 적용됨

## 해결방법

### updateMenu (메뉴 수정)
- menu_url이 null/빈값일 때 screen_menu_assignments의 is_active를 'N'으로 업데이트
- 화면 할당과 menu_url을 동기화하여 완전한 할당 해제

### saveMenu (메뉴 생성)
- 기존과 동일하게 menu_url이 없으면 screen_code를 null로 설정
- INSERT 시 screen_code 컬럼을 명시적으로 포함

## 메뉴 클릭 동작 순서
1. screen_menu_assignments 조회 (우선순위)
2. is_active='N'이면 할당된 화면 없음으로 간주
3. menu_url이 있으면 해당 URL로 이동
4. 둘 다 없으면 "연결된 페이지가 없습니다" 경고

이제 메뉴에서 URL을 제거하면 화면 할당도 완전히 해제됩니다.
2025-11-13 12:27:36 +09:00
kjs 3ddca95af5 feat: 메뉴 관리에서 화면 할당 해제 기능 추가
## 문제점
- URL 직접 입력 모드에서 빈 값으로 저장 시 menu_url은 비워지지만
- screen_code는 기존 값이 남아있어 화면 할당이 해제되지 않음

## 해결방법

### 백엔드 (adminController.ts)
- updateMenu: menu_url이 비어있으면 screen_code도 null로 자동 설정
- 로직: menuUrl ? screenCode : null

### 프론트엔드 (MenuFormModal.tsx, menu.ts)
- 화면 선택 시 screenCode도 함께 formData에 저장
- URL 타입 변경 시 screenCode 초기화
- MenuFormData 인터페이스에 screenCode 필드 추가

## 동작 방식
1. 화면 할당: menuUrl + screenCode 함께 저장
2. URL 직접 입력: menuUrl만 저장, screenCode는 undefined
3. 빈 값 저장: menuUrl = null, screenCode = null (자동)

이제 메뉴에서 화면 할당을 완전히 해제할 수 있습니다.
2025-11-13 12:22:33 +09:00
kjs 93bf83375e Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-13 12:17:28 +09:00
kjs 4e0d5239c6 커밋 2025-11-13 12:17:28 +09:00
kjs 658211b9d1 feat: 화면 복사 기능 개선 및 버튼 모달 설정 수정
## 주요 변경사항

### 1. 화면 복사 기능 강화
- 최고 관리자가 다른 회사로 화면 복사 가능하도록 개선
- 메인 화면과 연결된 모달 화면 자동 감지 및 일괄 복사
- 복사 시 버튼의 targetScreenId 자동 업데이트
- 일괄 이름 변경 기능 추가 (복사본 텍스트 제거)
- 중복 화면명 체크 기능 추가

#### 백엔드 (screenManagementService.ts)
- generateMultipleScreenCodes: 여러 화면 코드 일괄 생성 (Advisory Lock 사용)
- detectLinkedModalScreens: edit 액션도 모달로 감지하도록 개선
- checkDuplicateScreenName: 중복 화면명 체크 API 추가
- copyScreenWithModals: 메인+모달 일괄 복사 및 버튼 업데이트
- updateButtonTargetScreenIds: 복사된 모달로 버튼 targetScreenId 업데이트
- updated_date 컬럼 제거 (screen_layouts 테이블에 존재하지 않음)

#### 프론트엔드 (CopyScreenModal.tsx)
- 회사 선택 UI 추가 (최고 관리자 전용)
- 연결된 모달 화면 자동 감지 및 표시
- 일괄 이름 변경 기능 (텍스트 제거/추가)
- 실시간 미리보기
- 중복 화면명 체크

### 2. 버튼 설정 모달 화면 선택 개선
- 편집 중인 화면의 company_code 기준으로 화면 목록 조회
- 최고 관리자가 다른 회사 화면 편집 시 해당 회사의 모달 화면만 표시
- targetScreenId 문자열/숫자 타입 불일치 수정

#### 백엔드 (screenManagementController.ts)
- getScreens API에 companyCode 쿼리 파라미터 추가
- 최고 관리자는 다른 회사의 화면 목록 조회 가능

#### 프론트엔드
- ButtonConfigPanel: currentScreenCompanyCode props 추가
- DetailSettingsPanel: currentScreenCompanyCode 전달
- UnifiedPropertiesPanel: currentScreenCompanyCode 전달
- ScreenDesigner: selectedScreen.companyCode 전달
- targetScreenId 비교 시 parseInt 처리 (문자열→숫자)

### 3. 카테고리 메뉴별 컬럼 분리 기능
- 메뉴별로 카테고리 컬럼을 독립적으로 관리
- 카테고리 컬럼 추가/삭제 시 메뉴 스코프 적용

## 수정된 파일
- backend-node/src/services/screenManagementService.ts
- backend-node/src/controllers/screenManagementController.ts
- backend-node/src/routes/screenManagementRoutes.ts
- frontend/components/screen/CopyScreenModal.tsx
- frontend/components/screen/config-panels/ButtonConfigPanel.tsx
- frontend/components/screen/panels/DetailSettingsPanel.tsx
- frontend/components/screen/panels/UnifiedPropertiesPanel.tsx
- frontend/components/screen/ScreenDesigner.tsx
- frontend/lib/api/screen.ts
2025-11-13 12:17:10 +09:00
hyeonsu 565ab0b1c0 Merge pull request '지도 위젯(대시보드)수정' (#204) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/204
2025-11-13 12:14:33 +09:00
dohyeons bb9124d75b Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map 2025-11-13 12:10:54 +09:00
dohyeons 3b9327f64c 데이터 새로고침으로 수정 2025-11-13 12:10:04 +09:00
dohyeons 800bd85811 팝업 수정 가능하게 수정 2025-11-12 19:08:41 +09:00
kjs 06ef76814a Merge pull request 'feature/screen-management' (#203) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/203
2025-11-12 18:51:51 +09:00
kjs 2d3e7ba123 Merge branch 'main' into feature/screen-management 2025-11-12 18:51:44 +09:00
kjs b77fffbad7 리포트 모달문제 수정 2025-11-12 18:51:20 +09:00
dohyeons 5e8e714e8a 지도 위젯 헤더 및 새로고침 버튼 삭제 2025-11-12 18:22:02 +09:00
kjs 9040faa024 Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-12 18:02:18 +09:00
kjs 35024bd669 카테고리 구분 2025-11-12 18:02:17 +09:00
dohyeons c5d8569522 데이터소스 삭제 로직 수정 2025-11-12 17:58:22 +09:00
dohyeons adb1056b3f "지도가 표시되었습니다" 메시지 제거 2025-11-12 17:53:38 +09:00
kjs 95d3742507 Merge pull request 'feature/screen-management' (#202) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/202
2025-11-12 17:52:38 +09:00
kjs 4f2068a8af Merge branch 'main' into feature/screen-management 2025-11-12 17:52:31 +09:00
kjs 214bd829e9 테이블 컬럼추가 오류 수정 2025-11-12 17:52:08 +09:00
dohyeons d3b7859668 적용 버튼 비활성화 조건 추가 2025-11-12 17:39:03 +09:00
dohyeons 54b7cae218 polling 및 마커 종류 설정 추가 2025-11-12 17:21:08 +09:00
dohyeons 68184ac49f 에러 수정 2025-11-12 16:57:21 +09:00
kjs 77faba7e77 fix: 분할 패널 필터링 수정 및 디버깅 로그 제거
문제:
- 분할 패널에서 필터 입력 시 검색이 제대로 작동하지 않음
- 백엔드가 {value: '전자', operator: 'contains'} 형태를 처리하지 못함

원인:
- buildAdvancedSearchCondition이 필터 객체의 value 속성을 추출하지 않음
- 객체를 직접 문자열로 변환하여 '[object Object]'로 검색됨

해결:
1. tableManagementService.buildAdvancedSearchCondition 수정:
   - {value, operator} 형태의 필터 객체 감지
   - actualValue 추출 및 operator 처리
   - 모든 웹타입 케이스에 actualValue 전달

2. 프론트엔드 디버깅 로그 제거:
   - SplitPanelLayoutComponent의 console.log 제거
   - 필터, 컬럼 가시성, API 호출 로그 정리

테스트 필요:
- 분할 패널에서 필터 입력 → 정상 검색 확인
- 텍스트, 날짜, 숫자, 코드 타입 필터 동작 확인
2025-11-12 16:39:50 +09:00
kjs 7b84a81a96 fix: 분할 패널 컬럼 순서 변경 및 필터링 개선
문제:
1. ColumnVisibilityPanel에서 순서 변경 후 onColumnOrderChange가 호출되지 않음
2. 필터 입력 시 데이터가 제대로 필터링되지 않음
3. useAuth 훅 import 경로 오류 (@/hooks/use-auth → @/hooks/useAuth)

해결:
1. ColumnVisibilityPanel.handleApply()에 onColumnOrderChange 호출 추가
2. 필터 변경 감지 및 데이터 로드 로직 디버깅 로그 추가
3. useAuth import 경로 수정

테스트:
- 거래처관리 화면에서 컬럼 순서 변경 → 실시간 반영 
- 페이지 새로고침 → 순서 유지 (localStorage) 
- 필터 입력 → 필터 변경 감지 (추가 디버깅 필요)
2025-11-12 16:33:08 +09:00
kjs 579c4b7387 feat: 분할 패널 좌측 테이블에 검색/필터/그룹/컬럼가시성 기능 추가
문제:
- 분할 패널에서 검색 컴포넌트의 필터/그룹/컬럼 설정이 동작하지 않음
- 테이블 리스트 컴포넌트에 있던 로직이 분할 패널에는 없었음

해결:
1. 필터 처리:
   - leftFilters를 searchValues 형식으로 변환
   - API 호출 시 필터 조건 전달
   - 필터 변경 시 데이터 자동 재로드

2. 컬럼 가시성:
   - visibleLeftColumns useMemo 추가
   - leftColumnVisibility를 적용하여 표시할 컬럼 필터링
   - 렌더링 시 가시성 처리된 컬럼만 표시

3. 그룹화:
   - groupedLeftData useMemo 추가
   - leftGrouping 배열로 데이터를 그룹화
   - 그룹별 헤더와 카운트 표시

4. 테이블 등록:
   - columns 속성을 올바르게 참조 (displayColumns → columns)
   - 객체/문자열 타입 모두 처리
   - 화면 설정에 맞게 테이블 등록

테스트:
- 거래처 관리 화면에서 검색 컴포넌트 버튼 활성화
- 필터 설정 → 데이터 필터링 동작
- 그룹 설정 → 데이터 그룹화 동작
- 테이블 옵션 → 컬럼 가시성/순서 변경 동작
2025-11-12 16:13:26 +09:00
kjs 2dcf2c4c8e fix: 분할 패널 좌측 테이블 등록 시 displayColumns만 사용하도록 수정
문제:
- displayColumns가 비어있을 때 전체 컬럼을 보여주는 오류
- 화면에 설정된 컬럼만 표시되어야 함

해결:
- displayColumns가 비어있으면 테이블을 등록하지 않음
- displayColumns에 설정된 컬럼만 검색 컴포넌트에 등록
- 화면 관리에서 설정한 컬럼 구성을 정확히 반영

테스트:
- 거래처 관리 화면에서 좌측 테이블의 displayColumns(14개) 정상 표시
- 테이블 옵션/필터 설정/그룹 설정 버튼 정상 작동
- 우측 테이블은 검색 컴포넌트에서 제외
2025-11-12 16:05:45 +09:00
kjs 9cf9b87068 refactor: 분할 패널에서 좌측 테이블만 검색 컴포넌트 등록하도록 변경
변경 사유:
- 분할 패널은 마스터-디테일 구조로 좌측(마스터)만 독립적으로 검색 가능
- 우측(디테일)은 좌측 선택 항목에 종속되므로 별도 검색 불필요

변경 내용:
- 우측 테이블 registerTable 호출 제거 (주석 처리)
- TableSearchWidget에서 좌측 테이블만 선택 가능
- 우측 테이블 관련 상태(rightFilters, rightGrouping 등)는 내부 로직용으로 유지

효과:
- 분할 패널 사용 시 좌측 마스터 테이블만 검색 설정 가능
- 우측 디테일 테이블은 좌측 선택에 따라 자동 필터링
- 검색 컴포넌트 UI가 더 직관적으로 개선
2025-11-12 15:54:48 +09:00
kjs c40d8ea1ba fix: 분할 패널 우측 테이블 설정에서 화면에 표시되는 컬럼만 보이도록 수정
문제:
- 분할 패널의 테이블 옵션/검색필터 설정/그룹 설정에서 모든 컬럼이 표시됨
- 우측 패널에서 rightTableColumns (전체 컬럼) 사용하여 등록

해결:
- componentConfig.rightPanel?.columns (화면 표시 컬럼)만 등록하도록 수정
- 좌측 패널과 동일한 방식으로 displayColumns 사용
- 의존성 배열도 rightTableColumns → rightPanel.columns로 수정

변경 사항:
- rightTableColumns 대신 displayColumns 사용
- 컬럼 매핑 로직 개선 (col.columnName || col.name || col)
- 화면에 실제 표시되는 컬럼만 설정 UI에 노출
2025-11-12 15:49:52 +09:00
dohyeons cbdd9fef0f http polling 주기를 5초로 변경 2025-11-12 15:46:24 +09:00
kjs e06f21f63f refactor: 테이블 리스트 컴포넌트에서 검색 필터 UI 제거
- 테이블 상단 헤더 UI 전체 제거 (AdvancedSearchFilters 영역)
- 테이블 옵션, 필터 설정, 그룹 설정 버튼 제거
- 전체 개수 표시 제거
- 검색 필터 로직은 모두 유지 (상태, 함수, localStorage)
- 해당 기능은 TableSearchWidget 컴포넌트에서 제공 예정

변경 사유:
- 검색 필터 기능을 독립적인 TableSearchWidget으로 분리
- 테이블 컴포넌트와 검색 필터 UI의 관심사 분리
- 재사용 가능한 검색 필터 컴포넌트 구조로 개선
2025-11-12 15:45:21 +09:00
kjs df0929db60 Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-12 15:25:30 +09:00
kjs 54724fc578 fix: 그룹 설정 기능 수정 및 카테고리 값 표시 개선
- TableListComponent: grouping 상태 변경 시 groupByColumns 자동 업데이트
- 그룹화 시 카테고리/엔티티/코드 타입 컬럼은 _name 필드 사용
- 그룹 키 생성 시 실제 이름 표시 (코드 대신)

예시:
- 이전: 상태:CATEGORY_159712 > 품번:SLI-2025-0001
- 이후: 상태:완제품 > 품번:SLI-2025-0001
2025-11-12 15:25:21 +09:00
kjs 4fd05ddd59 Merge pull request 'feature/screen-management' (#201) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/201
2025-11-12 15:19:07 +09:00
kjs 436ec1c908 Merge branch 'main' into feature/screen-management 2025-11-12 15:19:00 +09:00
kjs 0f9cd93b8b Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-12 15:18:46 +09:00
kjs e723523ec5 fix: TypeScript 빌드 에러 수정
- tableManagementService: getTableDataWithEntityJoins options에 companyCode 타입 추가
- tableManagementController: Promise<void> 함수의 return 문 수정
- commonCodeService: CodeInfo 인터페이스에 menu_objid 필드 추가
- numberingRuleService: siblingObjids 변수 스코프 수정 (catch 블록 접근 가능하도록)
2025-11-12 15:18:32 +09:00
kjs 56cc2ff2e0 Merge pull request 'feature/screen-management' (#200) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/200
2025-11-12 15:12:33 +09:00
kjs 87efafb1c5 Merge branch 'main' into feature/screen-management 2025-11-12 15:12:27 +09:00
kjs 41404e021e Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-12 15:12:15 +09:00
kjs 4cdc72e360 로그 제거 2025-11-12 15:12:12 +09:00
kjs a883187889 feat: TableSearchWidget 높이 자동 조정 및 컴포넌트 재배치 기능 구현
- TableSearchWidgetHeightContext 추가: 위젯 높이 변화 관리
- TableSearchWidget: ResizeObserver로 높이 변화 감지 및 localStorage 저장
- 실제 화면(/screens/[screenId]/page.tsx)에서만 동작 (디자이너 제외)
- TableSearchWidget 아래 컴포넌트들의 Y 위치를 높이 차이만큼 자동 조정
- 화면 로딩 시 저장된 높이 복원 및 컴포넌트 위치 재조정

주요 변경사항:
1. 필터가 여러 줄로 wrap될 때 높이 자동 증가
2. 높이가 늘어난 만큼 아래 컴포넌트들이 자동으로 아래로 이동
3. 새로고침 후에도 설정 유지 (localStorage)
4. 화면 디자이너에서는 기존대로 동작 (영향 없음)

기술 구현:
- Context API로 위젯 높이 전역 관리
- ResizeObserver로 실시간 높이 감지
- localStorage로 사용자별 높이 설정 영구 저장
- 컴포넌트 렌더링 시 동적 Y 위치 계산
2025-11-12 14:54:49 +09:00
kjs 6d1743c524 feat: 테이블 검색 필터 개선 - 필터 너비 설정 및 자동 wrap 기능
- FilterPanel: 필터별 너비(width) 설정 기능 추가 (50-500px)
- TableSearchWidget: 필터가 여러 줄로 자동 wrap되도록 flex-wrap 적용
- TableSearchWidget: 필터 너비 설정을 localStorage에 저장/복원
- InteractiveScreenViewerDynamic: TableSearchWidget의 높이를 auto로 설정하여 콘텐츠에 맞게 자동 조정
- globals.css: 입력 필드 포커스 시 검정 테두리 제거 (combobox, input)

주요 변경사항:
1. 필터 설정에서 각 필터의 표시 너비를 개별 설정 가능
2. 필터가 많을 때 자동으로 여러 줄로 배치 (overflow 방지)
3. 설정된 필터 너비가 새로고침 후에도 유지됨
4. TableSearchWidget 높이가 콘텐츠에 맞게 자동 조정

TODO: TableSearchWidget 높이 변화 시 아래 컴포넌트 자동 재배치 기능 구현 예정
2025-11-12 14:50:06 +09:00
kjs 5c205753e2 feat: 테이블 검색 필터 UI 개선 및 실시간 검색 구현
- 모든 필터 입력창 높이 통일 (h-9, 36px)
- 실시간 검색: 입력 시 즉시 필터 적용 (검색 버튼 제거)
- 초기화 버튼 추가: 모든 필터값을 한번에 리셋
- filters → searchValues 자동 변환 로직 추가
- select 필터: 선택된 값의 라벨 저장하여 데이터 없을 때도 표시 유지
- select 옵션 초기 로드 후 계속 유지 (dataCount 변경 시에도 유지)

주요 개선사항:
1. Input, Select, Date 등 모든 필터의 높이가 동일하게 표시
2. 사용자가 값을 입력하면 바로 테이블이 필터링됨
3. 초기화 버튼으로 간편하게 모든 필터 제거 가능
4. 필터링 결과가 0건이어도 select 박스의 선택값이 유지됨

알려진 제한사항:
- 카테고리/엔티티 필터는 현재 테이블 데이터 기반으로만 옵션 표시
  (전체 정의된 카테고리 값이 아닌, 실제 데이터에 있는 값만 표시)
2025-11-12 14:16:16 +09:00
kjs 71fd3f5ee7 fix: 필터 select 옵션에서 카테고리/엔티티 라벨이 올바르게 표시되도록 수정
- 백엔드: entityJoinService에서 _label 필드를 SELECT에 추가
- 백엔드: tableManagementService에 멀티테넌시 필터링 추가 (company_code)
- 백엔드: categorizeJoins에서 table_column_category_values를 명시적으로 dbJoins로 분류
- 백엔드: executeCachedLookup와 getTableData에 companyCode 파라미터 추가
- 프론트엔드: getColumnUniqueValues가 백엔드 조인 결과의 _name 필드를 사용하도록 수정
- 프론트엔드: TableSearchWidget에서 select 옵션 로드 로직 개선

이제 필터 select 박스에서 코드 대신 실제 이름(라벨)이 표시됩니다.
예: CATEGORY_148700 → 정상, topseal_admin → 탑씰 관리자 계정
2025-11-12 14:02:58 +09:00
kjs 58870237b6 feat: 선택(select) 타입 필터 동적 옵션 로드 기능 추가
- TableRegistration에 getColumnUniqueValues 콜백 함수 추가
- TableListComponent에서 현재 데이터의 고유 값 추출 함수 구현
- TableSearchWidget에서 select 타입 필터의 옵션을 자동으로 로드
- 테이블 데이터가 변경되면 필터 옵션도 자동 업데이트
- 데이터 건수 표시 기능도 함께 수정 (등록 순서 문제 해결)
2025-11-12 12:06:58 +09:00
kjs 33ba13b070 fix: 테이블 컬럼 설정 개선
- 체크박스 컬럼 위치 보존 (드래그 순서 변경 시 맨 오른쪽으로 이동하는 문제 해결)
- 사용자별 컬럼 설정 localStorage 저장 및 불러오기 기능 추가
- useAuth 훅으로 userId 가져오기
- 초기 로드 시 저장된 설정 자동 복원
2025-11-12 11:15:44 +09:00
kjs 73049c4162 fix: 테이블 검색 필터 위젯 - 테이블 등록 및 선택 기능 수정
- TableListComponent: tableConfig.columns 기반 테이블 등록
- TableSearchWidget: 불필요한 로그 제거
- TableOptionsContext: 등록/해제 로그 제거
- TableListComponent 일부 카테고리 로그 제거 (진행중)
2025-11-12 10:58:21 +09:00
kjs c6941bc41f feat: 테이블 검색 필터 위젯 구현 완료
- TableOptionsContext 기반 테이블 자동 감지 시스템 구현
- 독립 위젯으로 드래그앤드롭 배치 가능
- 3가지 기능: 컬럼 가시성, 필터 설정, 그룹 설정
- FlowWidget, TableList, SplitPanel 등 모든 테이블 컴포넌트 지원
- 유틸리티 카테고리에 등록 (1920×80px)
- 위젯 크기 제어 가이드 룰 파일에 추가
2025-11-12 10:48:24 +09:00
kjs 379a3852b6 Merge pull request 'feature/screen-management' (#199) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/199
2025-11-11 18:33:39 +09:00
kjs f046493960 Merge branch 'main' into feature/screen-management 2025-11-11 18:33:34 +09:00
kjs fef2f4a132 fix: 화면 편집기에서 버튼 스타일 실시간 반영 문제 해결
**문제:**
- 화면 편집기에서 버튼의 스타일(색상, 폰트 등)을 변경해도 실시간으로 반영되지 않음
- 저장 후 실제 화면에서는 정상적으로 보임

**원인:**
- ButtonPrimaryComponent에서 isInteractive 모드일 때만 component.style을 적용
- 디자인 모드(isDesignMode)에서는 사용자 정의 스타일이 무시됨

**해결:**
- buttonElementStyle에 component.style을 항상 적용하도록 수정
- width/height는 레이아웃 충돌 방지를 위해 제외 유지
- 디자인 모드와 인터랙티브 모드 모두에서 스타일 실시간 반영

**영향:**
- 화면 편집기에서 버튼 스타일 변경 시 즉시 미리보기 가능
- 저장하지 않아도 시각적 피드백 제공
2025-11-11 18:27:27 +09:00
kjs 35ec16084f feat: 채번 규칙 및 코드 메뉴별 격리 구현
**주요 변경사항:**

1. **메뉴 스코프 변경 (getSiblingMenuObjids)**
   - 기존: 형제 메뉴 + 모든 형제의 자식 메뉴 포함
   - 변경: 자신 + 자신의 자식 메뉴만 포함
   - 결과: 각 2레벨 메뉴가 완전히 독립적으로 격리됨

2. **채번 규칙 메뉴 선택 상태 유지**
   - useState 초기값 함수에서 저장된 selectedMenuObjid 복원
   - 속성창 닫았다 열어도 선택한 메뉴와 채번 규칙 유지
   - config.autoGeneration.selectedMenuObjid에 저장

3. **로그 정리**
   - 프론트엔드: 디버깅 로그 제거
   - 백엔드: info 레벨 로그를 debug 레벨로 변경
   - 운영 환경에서 불필요한 로그 출력 최소화

**영향:**
- 영업관리 메뉴: 영업관리의 채번 규칙/코드만 조회
- 기준정보 메뉴: 기준정보의 채번 규칙/코드만 조회
- 각 메뉴 그룹이 독립적으로 데이터 관리 가능
2025-11-11 18:24:24 +09:00
hyeonsu 6364b337f6 Merge pull request '모달관련 에러 수정' (#198) from fix/modalError into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/198
2025-11-11 17:42:52 +09:00
dohyeons aeef1dc215 회원관련 로직 삭제 2025-11-11 17:42:22 +09:00
kjs 84f3ae4e6f fix: screenManagementService에서 queryOne import 누락 수정
- queryOne 함수를 db.ts에서 import하여 getMenuByScreen 함수가 정상 작동하도록 수정
2025-11-11 17:42:14 +09:00
dohyeons c18cd26ab4 모달 수정 2025-11-11 17:35:24 +09:00
kjs 6534d03ecd feat: 화면 편집기에서 메뉴 기반 데이터 스코프 적용
- 백엔드: screenManagementService에 getMenuByScreen 함수 추가
- 백엔드: GET /api/screen-management/screens/:id/menu 엔드포인트 추가
- 프론트엔드: screenApi.getScreenMenu() 함수 추가
- ScreenDesigner: 화면 로드 시 menu_objid 자동 조회
- ScreenDesigner: menuObjid를 RealtimePreview와 UnifiedPropertiesPanel에 전달
- UnifiedPropertiesPanel: menuObjid를 DynamicComponentConfigPanel에 전달

이로써 화면 편집기에서 코드/카테고리/채번규칙이 해당 화면이 할당된 메뉴 기준으로 필터링됨
2025-11-11 16:28:17 +09:00
kjs 32d4575fb5 feat: 코드 컴포넌트에 메뉴 스코프 적용
- useCodeOptions 훅에 menuObjid 파라미터 추가
- commonCodeApi.codes.getList에 menuObjid 전달
- SelectBasicComponent에서 menuObjid 받아서 useCodeOptions로 전달
- InteractiveScreenViewer에서 DynamicWebTypeRenderer로 menuObjid 전달
- 화면 페이지에서 RealtimePreview로 menuObjid 전달

이제 코드 위젯도 카테고리처럼 형제 메뉴별로 격리됩니다.
2025-11-11 15:25:07 +09:00
kjs 6ebe551caa feat: 카테고리 컴포넌트 메뉴 스코프 전환 완료
- 형제 메뉴의 카테고리 컬럼 조회 API 구현
- column_labels 테이블에서 컬럼 라벨 조회
- table_labels 테이블에서 테이블 라벨 조회
- 프론트엔드: 테이블명 대신 테이블 라벨 표시
- 카테고리 값 조회/추가 시 menuObjid 전달
2025-11-11 15:00:03 +09:00
kjs abdb6b17f8 debug: 카테고리 컬럼 조회 상세 로깅 추가
- 테이블 조회 완료 후 count 추가
- 카테고리 컬럼 쿼리 실행 전/후 로깅
- 에러 발생 시 전체 스택 트레이스 출력
2025-11-11 14:48:42 +09:00
kjs e7ecd0a863 fix: screen_menu_assignments를 통해 메뉴별 테이블 조회
 문제:
- screen_definitions 테이블에 menu_objid 컬럼이 없음
- SQL 쿼리 실행 실패 (500 에러)

 수정:
- screen_menu_assignments와 screen_definitions를 JOIN하여 조회
- menu_objid → screen_id → table_name 경로로 데이터 조회

🎯 쿼리 구조:
SELECT DISTINCT sd.table_name
FROM screen_menu_assignments sma
INNER JOIN screen_definitions sd ON sma.screen_id = sd.screen_id
WHERE sma.menu_objid = ANY($1)
  AND sma.company_code = $2
  AND sd.table_name IS NOT NULL
2025-11-11 14:47:25 +09:00
dohyeons 27e4fb3933 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node 2025-11-11 14:46:23 +09:00
kjs 23911d3dd8 feat: 카테고리 컴포넌트 메뉴 스코프 전환 완료
 구현 내용:
1. 백엔드 API 추가
   - GET /api/table-management/menu/:menuObjid/category-columns
   - 형제 메뉴들의 테이블에서 카테고리 타입 컬럼 조회
   - menuService.getSiblingMenuObjids() 재사용

2. 프론트엔드 CategoryWidget 수정
   - menuObjid를 props로 받아 CategoryColumnList에 전달
   - effectiveMenuObjid로 props.menuObjid도 처리
   - 선택된 컬럼에 tableName 포함하여 상태 관리

3. CategoryColumnList 수정
   - menuObjid 기반으로 형제 메뉴의 모든 카테고리 컬럼 조회
   - 테이블명+컬럼명 함께 표시
   - onColumnSelect에 tableName 전달

4. 메뉴 네비게이션 수정
   - AppLayout.tsx: 화면 이동 시 menuObjid를 URL 쿼리 파라미터로 전달
   - useMenu.ts: 동일하게 menuObjid 전달
   - page.tsx: 자식 컴포넌트에도 menuObjid 전달

🎯 효과:
- 이제 형제 메뉴들이 서로 다른 테이블을 사용해도 카테고리 공유 가능
- 메뉴 클릭 → 화면 이동 시 자동으로 menuObjid 전달
- 카테고리 위젯이 형제 메뉴의 모든 카테고리 컬럼 표시
2025-11-11 14:44:22 +09:00
kjs 668b45d4ea feat: 채번규칙 메뉴 스코프 전환 완료
 주요 변경사항:
- 백엔드: menuService.ts 추가 (형제 메뉴 조회 유틸리티)
- 백엔드: numberingRuleService.getAvailableRulesForMenu() 메뉴 스코프 적용
- 백엔드: tableCategoryValueService 메뉴 스코프 준비 (menuObjid 파라미터 추가)
- 프론트엔드: TextInputConfigPanel에 부모 메뉴 선택 UI 추가
- 프론트엔드: 메뉴별 채번규칙 필터링 (형제 메뉴 공유)

🔧 기술 세부사항:
- getSiblingMenuObjids(): 같은 부모를 가진 형제 메뉴 OBJID 조회
- 채번규칙 우선순위: menu (형제) > table > global
- 사용자 메뉴(menu_type='1') 레벨 2만 부모 메뉴로 선택 가능

📝 다음 단계:
- 카테고리 컴포넌트도 메뉴 스코프로 전환 예정
2025-11-11 14:32:00 +09:00
kjs 532c80a86b 분할패널 테이블 리스트 구현 2025-11-11 11:37:26 +09:00
kjs 5a5f86092f Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-11 10:46:41 +09:00
kjs bab960b50e feat: 엑셀 다운로드 파일명을 메뉴 이름으로 변경
- 메뉴 클릭 시 localStorage에 메뉴 이름 저장 (useMenu, AppLayout)
- 엑셀 다운로드 시 localStorage의 메뉴 이름을 파일명으로 사용
- 백엔드 카테고리 컬럼 조회 쿼리 파라미터 버그 수정
- API 호출 시 불필요한 autoFilter 파라미터 제거

파일명 형식: {메뉴이름}_{날짜}.xlsx
예시: 품목등록테스트_2025-11-11.xlsx
2025-11-11 10:29:47 +09:00
kjs f6edf8c313 Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-10 18:38:57 +09:00
kjs c5b065ac81 리스트 스크롤 제거 2025-11-10 18:38:56 +09:00
kjs 4ac3f66e7d Merge pull request 'feature/screen-management' (#197) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/197
2025-11-10 18:26:18 +09:00
kjs 0c9356813e Merge branch 'main' into feature/screen-management 2025-11-10 18:25:50 +09:00
kjs 8dee8ac314 Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-10 18:25:36 +09:00
kjs 59fa54b812 style: 테이블 리스트 폰트 및 여백 조정
- 데이터 행에 font-normal 적용하여 bold 제거
- 헤더는 font-bold 유지
- 데이터 행 상하 여백 축소 (py-2 → py-1.5)
- 행 고정 높이 제거하여 컨텐츠에 따라 자동 조정

변경된 파일:
- frontend/lib/registry/components/table-list/TableListComponent.tsx
2025-11-10 18:24:51 +09:00
kjs 2722ebb218 feat: 테이블 리스트 날짜 형식을 yyyy-mm-dd로 변경
- inputType이 date/datetime인 컬럼 yyyy-mm-dd 형식으로 표시
- format이 'date'인 경우도 동일한 형식 적용
- 생성일, 수정일 등 날짜 컬럼 가독성 개선

변경된 파일:
- frontend/lib/registry/components/table-list/TableListComponent.tsx
2025-11-10 18:15:06 +09:00
kjs dad7e9edab feat: 엑셀 다운로드 기능 개선
- 화면 편집기 컬럼 설정 기반 다운로드 (visible 컬럼만)
- 필터 조건 적용 (필터링된 데이터만 다운로드)
- 한글 라벨명 표시 (column_labels 테이블 조회)
- Entity 조인 값 표시 (writer → writer_name 등)
- 카테고리 타입 라벨 변환 (코드 → 라벨)
- 멀티테넌시 보안 강화 (autoFilter: true)
- 디버깅 로그 정리

변경된 파일:
- frontend/lib/utils/buttonActions.ts
- frontend/lib/registry/components/table-list/TableListComponent.tsx

관련 이슈: #엑셀다운로드개선
2025-11-10 18:12:09 +09:00
dohyeons 786d71a697 feat: 회원가입 페이지 및 폼 유효성 검사 구현
- 회원가입 페이지 및 폼 컴포넌트 추가
  - 로그인 페이지와 일관된 디자인
  - 아이디, 비밀번호, 이름, 차량번호, 휴대폰번호 입력 필드
  - 비밀번호 확인 필드 추가

- 유효성 검사 기능 구현
  - 차량번호: 한국 차량번호 형식 검증 (12가3456, 123가4567, 서울12가3456 등)
  - 휴대폰번호: 하이픈 포함 형식 검증 (010-1234-5678)
  - 비밀번호: 최소 6자 이상, 확인 일치 검증
  - 사용자ID: 최소 4자 이상
  - 이름: 최소 2자 이상

- UI/UX 개선
  - 각 필드별 실시간 유효성 검사 및 에러 메시지 표시
  - 비밀번호 표시/숨김 토글 버튼
  - 자동 설정된 필드에 안내 문구 표시
  - 로그인 페이지로 돌아가기 버튼 추가
  - 로그인 페이지에 회원가입 링크 추가

- 타입 및 훅 추가
  - RegisterFormData, RegisterResponse 타입 정의
  - useRegister 훅으로 비즈니스 로직 분리
  - auth API mock 함수 (백엔드 연동 준비)

- 사용자 경험 고려
  - 입력 필드별 placeholder 예시 제공
  - 도움말 텍스트로 입력 형식 안내
  - 로딩 상태 표시
2025-11-10 17:12:47 +09:00
kjs 49f779e0e4 feat: writer 컬럼 자동 user_name 변환 완료
- writer 컬럼이 있는 테이블에서 자동으로 user_name 표시
- 백엔드: entityJoinService에서 writer 컬럼 감지 및 user_info 조인
- 프론트엔드: entityJoinApi 항상 사용 및 writer_name 자동 표시
- 디버깅 로그 제거
2025-11-10 16:38:16 +09:00
kjs 605fbc4383 debug: writer 컬럼 조인 디버깅 로그 추가
- 프론트엔드: formatCellValue에서 writer 컬럼 데이터 로깅
- 백엔드: writer 조인 설정 및 검증 상세 로깅
- 목적: writer_name이 빈값으로 표시되는 문제 원인 파악
2025-11-10 16:36:54 +09:00
kjs 2e0ccaac16 fix: 모든 테이블 데이터 조회 시 entityJoinApi 사용하도록 수정
- 문제: writer 컬럼이 user_name으로 변환되지 않음
- 원인: entityJoinColumns가 없을 때 tableTypeApi 사용 (entity 조인 미지원)
- 해결: 항상 entityJoinApi.getTableDataWithJoins 사용
- 영향:
  - writer 컬럼이 있는 모든 테이블에서 자동으로 writer_name 조인
  - 기존 additionalJoinColumns도 정상 작동
  - 백엔드의 자동 writer 조인 기능 활성화
2025-11-10 16:33:15 +09:00
kjs ccbb6924c8 feat: writer 컬럼 자동 user_name 변환 기능 추가
- 문제: 테이블 리스트에서 writer 컬럼이 user_id로 표시됨
- 해결:
  1. 백엔드: entityJoinService에서 writer 컬럼 자동 감지
  2. writer 컬럼 발견 시 user_info 테이블과 자동 조인
  3. writer_name 별칭으로 user_name 반환
  4. 프론트엔드: writer 컬럼일 때 writer_name 우선 표시
- 영향:
  - writer 컬럼이 있는 모든 테이블에서 자동으로 작성자명 표시
  - 기존 entity 조인 설정과 충돌 없이 작동
  - column_labels 설정 불필요
2025-11-10 16:32:00 +09:00
kjs 0e95f8ed66 fix: RealtimePreviewDynamic에서 component.style의 width/height가 size를 덮어쓰는 문제 수정
- 문제: 속성 패널에서 너비 입력 시 size.width는 변경되지만 화면에 반영되지 않음
- 원인: RealtimePreviewDynamic의 baseStyle에서 componentStyle을 getWidth() 이후에 스프레드하여 size.width를 덮어씀
- 해결:
  1. componentStyle에서 width, height 제거
  2. 나머지 스타일만 먼저 적용
  3. getWidth(), getHeight()로 size 기반 크기를 마지막에 설정
- 영향:
  - 속성 패널에서 입력한 너비/높이가 화면에 즉시 반영됨
  - component.style의 width/height는 무시되고 size.width/height만 사용됨
- 디버깅 로그 제거
2025-11-10 16:09:38 +09:00
kjs 8e74429a83 fix: updateComponentProperty에서 gridColumns 관련 자동 크기 조정 제거
- 문제: 속성 패널에서 너비를 입력해도 화면에 반영되지 않음
- 원인: updateComponentProperty에서 gridColumns 변경 시 자동으로 너비를 재계산
- 해결:
  1. gridColumns 변경 시 updateSizeFromGridColumns 호출 제거
  2. gridColumns 변경 시 calculateWidthFromColumns 호출 제거
- 영향:
  - 속성 패널에서 입력한 너비가 화면에 즉시 반영됨
  - gridColumns는 더 이상 너비를 자동으로 조정하지 않음
2025-11-10 15:58:56 +09:00
kjs 2148e8e019 fix: 너비 입력 시 완전 자유 입력 허용 (로컬 상태 사용)
- 문제: 너비 입력 시 onChange에서 즉시 업데이트되어 30에서 3을 지우기 어려움
- 해결:
  1. localWidth 상태 추가
  2. onChange: 로컬 상태만 업데이트 (완전 자유 입력)
  3. onBlur/Enter: 실제 업데이트 + 10px 단위 스냅
  4. useEffect로 컴포넌트 변경 시 localWidth 동기화
- 영향:
  - 30 입력 시 3, 0 모두 자유롭게 지우고 입력 가능
  - 포커스 아웃 시에만 10px 단위로 정렬
2025-11-10 15:54:07 +09:00
kjs 5d374f902a fix: 너비/높이 입력 시 자유 입력 가능하도록 수정 및 포커스 아웃 시 10px 단위 스냅
- 문제: 너비/높이 입력 시 즉시 10px 단위로 스냅되어 자유 입력 불가
- 해결:
  1. 너비: onChange에서는 입력값 그대로 반영, onBlur에서 10px 단위로 스냅
  2. 높이: 로컬 상태로 자유 입력 허용, onBlur/Enter 시 10px 단위로 스냅
  3. step을 10에서 1로 변경하여 자유 입력 가능
- 영향:
  - 입력 중에는 원하는 값 입력 가능
  - 입력 완료 시(포커스 아웃 또는 Enter) 자동으로 10px 단위로 정렬
2025-11-10 15:51:37 +09:00
kjs 99468ca250 fix: 속성 패널에서 너비/높이 직접 입력 시 격자 스냅 제거
- 문제: 속성 패널에서 너비/높이 입력 시 격자 시스템이 자동으로 값을 변경
- 원인: updateComponentProperty에서 size.width/height 변경 시 무조건 격자 스냅 적용
- 해결: 직접 입력 시에는 격자 스냅을 적용하지 않도록 로직 주석 처리
- 영향:
  - 속성 패널에서 원하는 크기로 자유롭게 설정 가능
  - 드래그/리사이즈 시에는 별도 로직에서 격자 스냅 처리
- 디버깅 로그 제거
2025-11-10 15:49:48 +09:00
kjs 99deab05d8 fix: gridUtils 함수들 import 누락 수정
- 문제: adjustGridColumnsFromSize 등 gridUtils 함수들이 import되지 않아 런타임 오류 발생
- 해결:
  1. gridUtils에서 필요한 함수들 import 추가
  2. 3개 파라미터를 받는 snap 함수 호출을 올바른 함수로 변경
     - snapSizeTo10px -> snapSizeToGrid
     - snapPositionTo10px -> snapToGrid
- 영향: 컴포넌트 크기/위치 조정 시 격자 스냅 기능 정상 작동
2025-11-10 15:45:51 +09:00
kjs 5f11b5083f fix: gridInfo 미정의 오류 수정
- 문제: updateComponentProperty 함수 내에서 정의되지 않은 gridInfo 변수 참조
- 해결: gridInfo 조건을 prevLayout.gridSettings 체크로 변경
- 영향: 컴포넌트 속성 업데이트 시 런타임 오류 해결
2025-11-10 15:42:35 +09:00
kjs cdf9c0e562 fix: 화면 편집기에서 버튼 컴포넌트 선택 가능하도록 수정
- 문제: 버튼 컴포넌트 클릭 시 버튼 동작이 실행되어 선택되지 않음
- 해결:
  1. ButtonPrimaryComponent에서 디자인 모드일 때 <button> 대신 <div>로 렌더링
  2. ScreenDesigner의 ScreenPreviewProvider에서 isPreviewMode를 false로 설정
  3. 디자인 모드에서는 버튼 액션이 실행되지 않고 onClick만 전달되도록 수정
- 영향: button-primary 타입 버튼이 화면 편집기에서 정상적으로 선택 가능
2025-11-10 15:36:18 +09:00
kjs 2d832c56b6 feat: 차지 컬럼 수를 픽셀 기반 너비 입력으로 변경 (10px 단위) 2025-11-10 15:17:33 +09:00
kjs 1d26b979ac fix: handleComponentDrop에서 gridInfo 참조 제거 및 10px 스냅 적용 2025-11-10 15:10:42 +09:00
kjs 2a2bf86d12 fix: calculateGridInfo 더미 함수 추가 및 드래그 앤 드롭 수정 2025-11-10 15:09:27 +09:00
kjs d7e598435c fix: UnifiedPropertiesPanel에서도 컬럼 수/간격/여백 설정 제거 2025-11-10 15:06:46 +09:00
kjs 0af0b53638 fix: 컬럼 수, 간격, 여백 설정 완전 제거 (10px 고정) 2025-11-10 15:05:34 +09:00
kjs ed351f7044 fix: layout.gridSettings 문법 오류 수정 2025-11-10 14:54:53 +09:00
kjs d0ddc702ac fix: 모든 snapToGrid 문법 오류 최종 수정 2025-11-10 14:52:20 +09:00
kjs eb8e5da329 fix: console.log 내 snapToGrid 문법 오류 수정 2025-11-10 14:51:36 +09:00
kjs e7cbbe39a6 fix: 마지막 snapToGrid 문법 오류 수정 2025-11-10 14:51:01 +09:00
kjs 8f41cf7919 fix: 모든 snapToGrid 문법 오류 수정 2025-11-10 14:50:24 +09:00
kjs 4cd9629a1d fix: snapToGrid 문법 오류 수정 - 항상 true로 설정 2025-11-10 14:48:53 +09:00
kjs 7f68a70b0f wip: snapToGrid 의존성 제거 2025-11-10 14:47:59 +09:00
kjs 0474937e57 wip: gridInfo 의존성 제거 2025-11-10 14:46:30 +09:00
kjs d8bba7cfc1 wip: 격자 함수 호출을 10px 스냅으로 일괄 교체
- snapToGrid -> snapPositionTo10px
- snapSizeToGrid -> snapSizeTo10px
- 격자 라인을 10px 단위로 변경
- gridInfo 의존성 제거 (진행중)
2025-11-10 14:45:19 +09:00
kjs 554cdbdea5 wip: 격자 시스템 제거 시작 - 10px 스냅 함수 추가 2025-11-10 14:43:09 +09:00
kjs c4290f2d0e refactor: 격자 시스템을 10px 단위로 단순화
- 복잡한 컬럼 시스템 제거
- 웹타입별 고정 픽셀 너비 사용 (10px 단위)
- 격자 설정 패널 단순화 (컬럼 수 설정 제거)
- 간격/여백 조정을 10px 단위로 변경
- 더 직관적이고 예측 가능한 레이아웃 시스템
2025-11-10 14:41:58 +09:00
kjs 7815a34de4 Merge pull request 'feature/screen-management' (#195) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/195
2025-11-10 14:37:58 +09:00
kjs 7dc420a1a2 Merge branch 'main' into feature/screen-management 2025-11-10 14:37:50 +09:00
kjs 7ab3781372 feat: 드래그 드롭 컬럼 라벨 숨김 및 placeholder 표시
- 테이블 탭에서 드래그 드롭으로 배치되는 컬럼의 라벨 자동 숨김 (labelDisplay: false)
- placeholder에 컬럼 라벨명 자동 표시
- 폼 컨테이너 및 캔버스 직접 드롭 모두 적용
- 더 깔끔한 UI 제공
2025-11-10 14:36:53 +09:00
kjs 3f32996014 fix: 날짜 입력 필드 높이 반응형 개선
- 드래그 드롭으로 배치되는 인풋 필드 기본 높이 30px로 변경
- 날짜 입력 컴포넌트의 모든 외부 div에 h-full 추가
- 모든 input 요소에 min-h-full 추가하여 부모 높이를 제대로 따르도록 수정
- daterange, year 타입도 동일하게 적용
2025-11-10 14:33:15 +09:00
kjs a868c5c413 feat: 테이블 탭에서 시스템 컬럼 5개 숨김 처리
- id, created_date, updated_date, writer, company_code 컬럼 필터링
- 대소문자 구분 없이 시스템 컬럼 제외
- 화면 편집기 테이블 탭에서 비즈니스 컬럼만 표시
2025-11-10 14:24:16 +09:00
kjs 15f21a1142 revert: e27845a 커밋의 변경사항 되돌림 - 화면 레이아웃 문제 수정 2025-11-10 14:21:29 +09:00
dohyeons 873addb96a feat: 원본 최신 소스 반영 + NCP Kubernetes 배포 설정 추가
- 원본 ERP-node 프로젝트의 최신 코드 반영
- NCP Kubernetes 배포를 위한 인프라 파일 추가
  - Jenkinsfile: Kaniko 기반 CI/CD 파이프라인
  - Dockerfile: 멀티스테이지 빌드 (백엔드 + 프론트엔드)
  - values_logistream.yaml: Helm 차트 values 파일
  - DEPLOYMENT_GUIDE_KPSLP.md: 배포 가이드 문서
- 불필요한 마이그레이션 계획 문서 36개 정리
2025-11-10 14:20:10 +09:00
kjs 02644f38ee Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-10 12:00:21 +09:00
kjs ce3ba22c54 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-10 12:00:08 +09:00
kjs 61dc48e638 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-10 11:59:57 +09:00
hyeonsu 4b540dc587 Merge pull request 'feat/screenDesinger' (#196) from feat/screenDesinger into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/196
2025-11-10 11:57:32 +09:00
dohyeons e9f0244210 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feat/screenDesinger 2025-11-10 11:56:56 +09:00
dohyeons 68c3db5213 feat: 분할 패널 레이아웃 멀티테넌시 및 자동 필터링 기능 추가
- 데이터 조회 API에 회사별 자동 필터링 기능 추가
  - GET /api/data/:tableName에 company_code 필터 자동 적용
  - GET /api/data/join에 우측 테이블 회사별 필터링 추가
  - 최고 관리자(company_code = '*')는 전체 데이터 조회 가능

- 분할 패널 레이아웃 우측 추가 시 조인 컬럼 자동 입력
  - 좌측에서 선택한 항목의 조인 키 값을 우측 추가 모달에 자동 설정
  - 자동 설정된 필드는 읽기 전용으로 표시 (disabled + 안내 문구)
  - 사용자는 나머지 필드만 입력하면 됨

- 데이터 서비스 개선
  - getJoinedData 함수에 companyCode 파라미터 추가
  - checkColumnExists 함수를 public으로 변경하여 재사용성 향상
  - 조인 쿼리에 DISTINCT 추가로 중복 데이터 방지
  - 복합키 테이블의 레코드 삭제 지원

- 레코드 생성 시 멀티테넌시 자동 처리
  - company_code와 company_name 자동 추가
  - 테이블 컬럼 존재 여부 체크 후 자동 설정

- 분할 패널 설정 UI 개선
  - 좌측 패널 표시 컬럼 선택 UI 추가
  - 추가 폼에 표시할 컬럼 선택 기능 추가
  - Primary Key 정보 자동 조회 및 표시
2025-11-10 11:56:39 +09:00
kjs 94846e92ef Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-10 09:36:05 +09:00
kjs e2f4b47588 모달 잘 보이게 수정 2025-11-10 09:33:29 +09:00
hyeonsu e10d6a3b94 Merge pull request 'feat/screenDesinger' (#194) from feat/screenDesinger into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/194
2025-11-07 18:21:04 +09:00
dohyeons 68577a09f9 우측 패널 항목 삭제 기능 구현 2025-11-07 18:20:24 +09:00
kjs 1d6418ca63 fix: SaveModal을 ResizableDialog로 수정하여 크기 조절 가능하도록 개선
주요 변경사항:
- Dialog/DialogContent를 ResizableDialog/ResizableDialogContent로 변경
- DialogTitle을 ResizableDialogTitle로 변경
- 내부 컨텐츠 컨테이너를 유연한 크기(w-full h-full)로 변경
- minWidth/minHeight 사용으로 최소 크기 보장

참고:
- 컴포넌트 레이아웃이 화면관리에서 설정된 대로 정확히 렌더링됨
- 레이아웃 자체의 문제는 화면관리에서 재설계 필요

파일 변경:
- frontend/components/screen/SaveModal.tsx
2025-11-07 17:39:51 +09:00
kjs e27845a82f feat: 테이블 탭 드래그앤드롭 개선 및 AI-개발자 협업 규칙 수립
주요 변경사항:
- 드래그앤드롭 컬럼의 라벨 숨김 및 placeholder로 라벨명 표시
- 기본 높이 30px로 변경
- 5개 시스템 컬럼(id, created_date, updated_date, writer, company_code) 숨김
- AI-개발자 협업 작업 수칙 문서 작성 및 .cursorrules에 통합

파일 변경:
- frontend/components/screen/ScreenDesigner.tsx
  * getDefaultHeight(): 기본 높이를 30px로 변경
  * handleDrop(): labelDisplay false, placeholder 추가
- frontend/components/screen/panels/TablesPanel.tsx
  * hiddenColumns Set으로 시스템 컬럼 필터링
- .cursor/rules/ai-developer-collaboration-rules.mdc (신규)
  * 확인 우선, 한 번에 하나, 철저한 마무리 원칙
  * 데이터베이스 검증, 코드 수정, 테스트, 커뮤니케이션 규칙
- .cursorrules
  * 필수 확인 규칙 섹션 추가
  * 모든 작업 시작/완료 시 협업 규칙 확인 강제화
2025-11-07 17:12:01 +09:00
dohyeons 3009d1eecc 삭제버튼 수정버튼 토글 삭제 2025-11-07 16:09:58 +09:00
dohyeons afea879920 수정/삭제 기능 구현 2025-11-07 16:02:01 +09:00
dohyeons 672aba8404 계층 구조 트리 뷰 2025-11-07 15:21:44 +09:00
kjs 4294fbf608 feat: 채번 규칙 테이블 기반 자동 필터링 구현
- 채번 규칙 scope_type을 table로 단순화
- 화면의 테이블명을 자동으로 감지하여 채번 규칙 필터링
- TextInputConfigPanel에 screenTableName prop 추가
- getAvailableNumberingRulesForScreen API로 테이블 기반 조회
- NumberingRuleDesigner에서 자동으로 테이블명 설정
- webTypeConfigConverter 유틸리티 추가 (기존 화면 호환성)
- AutoGenerationConfig 타입 개선 (enabled, options.numberingRuleId)
- 채번 규칙 선택 UI에서 ID 제거, 설명 추가
- 불필요한 console.log 제거

Backend:
- numberingRuleService: 테이블 기반 필터링 로직 구현
- numberingRuleController: available-for-screen 엔드포인트 수정

Frontend:
- TextInputConfigPanel: 테이블명 기반 채번 규칙 로드
- NumberingRuleDesigner: 적용 범위 UI 제거, 테이블명 자동 설정
- ScreenDesigner: webTypeConfig → autoGeneration 변환 로직 통합
- DetailSettingsPanel: autoGeneration 속성 매핑 개선
2025-11-07 14:27:07 +09:00
dohyeons efaa267d78 분할 패널에서 부서 추가 기능 구현 2025-11-07 14:22:23 +09:00
dohyeons 7835898a09 우측 패널 조절 가능하도록 수정 2025-11-07 12:00:46 +09:00
dohyeons 25740c499d 좌측 패널에 매핑한 컬럼 나오도록 구현 2025-11-07 11:51:44 +09:00
kjs 5b79bfb19d 화면 컴포넌트 위치문제 수정 2025-11-07 11:36:58 +09:00
dohyeons 03bce9d643 프론트 빌드 에러 해결 2025-11-07 11:05:19 +09:00
dohyeons 732928ac0f 프론트 필드 에러 해결 2025-11-07 10:32:30 +09:00
dohyeons 35f130061a 빌드 오류 해결 2025-11-07 10:22:49 +09:00
dohyeons 920cdccdf9 빌드 에러 해결 2025-11-07 10:18:34 +09:00
hjlee 0313c83a65 Merge pull request '오늘의 타협점' (#193) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/193
2025-11-06 19:01:58 +09:00
leeheejin 20e2729bf7 오늘의 타협점 2025-11-06 19:01:44 +09:00
kjs 242e5bee41 Merge pull request 'feature/screen-management' (#192) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/192
2025-11-06 18:35:21 +09:00
kjs fb201cc799 회사별 테이블 데이터 격리 2025-11-06 18:35:05 +09:00
kjs 0e4cf7b641 쿼리 에러 수정 2025-11-06 18:10:21 +09:00
kjs 5d9233203c Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-06 18:10:05 +09:00
hjlee 15f35d8d94 Merge pull request '화면 고치기' (#191) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/191
2025-11-06 18:08:42 +09:00
leeheejin 2f39b541dd 화면 고치기 2025-11-06 18:08:28 +09:00
kjs a2637f4dbb Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-06 17:33:29 +09:00
hjlee 485780c57c Merge pull request 'lhj' (#190) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/190
2025-11-06 17:32:51 +09:00
leeheejin ead3433f3e 기능추가 2025-11-06 17:32:29 +09:00
leeheejin b3cd771b99 버튼 수정과 그룹드롭다운, 품목복사기능, 연속입력기능추가 2025-11-06 17:32:24 +09:00
kjs f2500865a6 격자 저장문제 수정 2025-11-06 17:01:13 +09:00
kjs c22e38da76 Merge pull request 'feature/screen-management' (#189) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/189
2025-11-06 14:46:33 +09:00
kjs 786576bb76 커밋 2025-11-06 14:46:15 +09:00
kjs 832e80cd7f 배지 표시 수정 2025-11-06 14:18:36 +09:00
kjs 2e674e13d0 fix: resizable-dialog 주석 처리된 객체 리터럴 파싱 에러 수정
- 여러 줄 객체 리터럴을 한 줄로 변경
- console.log 주석이 파싱 에러를 일으키는 문제 해결
- 빌드 에러 해결
2025-11-06 13:26:54 +09:00
kjs bc826e8e49 chore: resizable-dialog 디버깅 로그 모두 제거
- console.log 20개 주석 처리
- 콘솔 스팸 방지
- 불필요한 로그 제거로 성능 개선
2025-11-06 12:46:08 +09:00
kjs 4affe623a5 fix: 카테고리 매핑 로딩 타이밍 개선
- loading 의존성 제거 (불필요한 재로드 방지)
- columnMeta 길이 변화로 매핑 로드 트리거
- 매핑 로드 전후 상태 디버깅 로그 추가
- categoryMappings 빈 객체 문제 해결
2025-11-06 12:43:01 +09:00
kjs f53a818f2f fix: 카테고리 매핑 변경 시 강제 리렌더링 추가
- categoryMappingsKey 상태 추가로 매핑 변경 감지
- 매핑 업데이트 시 key 증가로 tbody 리렌더링 강제
- 간헐적으로 배지가 표시되지 않던 타이밍 이슈 해결
- 카테고리 배지 렌더링 디버깅 로그 추가
2025-11-06 12:39:56 +09:00
kjs b5a83bb0f3 docs: inputType 사용 가이드 추가
- webType은 레거시, inputType만 사용해야 함을 명시
- API 호출 및 캐시 처리 방법 설명
- 실제 적용 사례 및 마이그레이션 체크리스트 포함
- 디버깅 팁 및 주요 inputType 종류 문서화
2025-11-06 12:32:17 +09:00
kjs 85e1b532fa fix: 캐시에서 inputType 누락 문제 해결
- 캐시된 데이터 사용 시 inputType이 설정되지 않던 문제 수정
- cached.inputTypes를 올바르게 매핑하여 meta에 포함
- webType 체크 제거, inputType만 사용하도록 변경
- 화면 전환 후 캐시 사용 시에도 카테고리 타입 정상 인식
2025-11-06 12:28:39 +09:00
kjs 4cd08c3900 fix: webType도 체크하여 카테고리 컬럼 감지
- inputType과 webType 모두 'category'인 경우 처리
- columnMeta에 inputType이 없어도 webType으로 감지 가능
- material 컬럼 등 webType만 있는 경우도 정상 동작
2025-11-06 12:27:22 +09:00
kjs 70dc24f7a1 fix: columnMeta 로딩 완료 후 카테고리 매핑 로드
- columnMeta가 비어있을 때 로딩 대기 로그 출력
- columnMeta 준비 완료 후에만 카테고리 매핑 시도
- 카테고리 컬럼 없음 로그에 디버깅 정보 추가
- 화면 전환 시 columnMeta → 카테고리 매핑 순서 보장
2025-11-06 12:26:07 +09:00
kjs cd961a2162 fix: 화면 복귀 시 카테고리 매핑 갱신 보장
- loading 상태를 의존성으로 변경
- 데이터 로드 완료 시점(loading: false)에 매핑 갱신
- 화면 전환 후 복귀 시에도 최신 카테고리 데이터 반영
- 로딩 중에는 매핑 로드하지 않도록 가드 추가
2025-11-06 12:24:12 +09:00
kjs 95b341df79 fix: 데이터 변경 시 카테고리 매핑 자동 갱신
- useEffect 의존성을 refreshTrigger에서 data.length로 변경
- 데이터가 추가/삭제/변경될 때마다 자동으로 매핑 갱신
- 화면 전환 후 데이터 로드 완료 시점에 매핑도 함께 갱신
2025-11-06 12:22:24 +09:00
kjs 49935189b6 fix: 화면 전환 후 카테고리 매핑 갱신 문제 해결
- useEffect 의존성 배열에 refreshTrigger 추가
- 데이터 새로고침 시 카테고리 매핑도 자동 갱신
- 매핑 로드 시작/종료 로그 추가하여 디버깅 용이성 향상
2025-11-06 12:20:58 +09:00
kjs 939a8696c8 feat: TableListComponent에서 카테고리 값을 배지로 표시
- categoryMappings 타입을 색상 포함하도록 수정
- 카테고리 값 로드 시 color 필드 포함
- formatValue에서 카테고리를 Badge 컴포넌트로 렌더링
- 매핑 없을 시에도 기본 slate 색상의 배지로 표시
- 디버깅 로그 추가
2025-11-06 12:18:43 +09:00
hjlee 9f4e71fc68 Merge pull request 'lhj' (#188) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/188
2025-11-06 12:18:21 +09:00
kjs b526d8ea2c fix: 카테고리 배지 표시 개선 및 디버깅 로그 추가
- 매핑이 없어도 항상 배지로 표시
- 매핑 없을 시 코드값 그대로 + 기본 slate 색상 사용
- 카테고리 매핑 로드 과정 로그 추가
- 기존 데이터에 기본 색상 추가하는 마이그레이션 스크립트 생성
2025-11-06 12:15:47 +09:00
leeheejin 3f890cdbfa Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Conflicts:
;	frontend/components/admin/CreateTableModal.tsx
;	frontend/components/screen/CopyScreenModal.tsx
;	frontend/components/screen/MenuAssignmentModal.tsx
;	frontend/components/screen/ScreenList.tsx
;	frontend/components/screen/widgets/FlowWidget.tsx
;	frontend/lib/registry/components/table-list/TableListComponent.tsx
2025-11-06 12:14:07 +09:00
kjs 7581cd1582 feat: 테이블 리스트에서 카테고리 값을 배지로 표시
- 카테고리 타입 컬럼을 배지 형태로 렌더링
- 사용자가 설정한 색상 적용
- categoryMappings에 라벨과 색상 모두 저장
- 기본 색상: #3b82f6 (파란색)
- 텍스트 색상: 흰색으로 고정하여 가독성 확보
2025-11-06 12:12:19 +09:00
leeheejin 0839f7f603 리사이징, 체크박스,엔터치면 다음 칸으로 이동, 표수정, 컬럼에서 이미지 넣는거 등등 2025-11-06 12:11:49 +09:00
kjs 1d87b6c3ac feat: 카테고리 값에 배지 색상 설정 기능 추가
- 카테고리 값 추가/편집 다이얼로그에 색상 선택기 추가
- 18가지 기본 색상 팔레트 제공
- 선택한 색상의 실시간 배지 미리보기
- color 필드를 통해 DB에 저장
- 테이블 리스트에서 배지 형태로 표시할 준비 완료
2025-11-06 12:09:28 +09:00
kjs 4b2514d9da Merge pull request 'fix: 카테고리 타입 컬럼 라벨 표시 및 빌드 오류 수정' (#187) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/187
2025-11-06 12:04:19 +09:00
kjs 7cc325edd5 Merge branch 'main' into feature/screen-management 2025-11-06 12:04:12 +09:00
kjs a1cb9d2a8e fix: 카테고리 타입 컬럼 라벨 표시 및 빌드 오류 수정
- 카테고리 타입 컬럼이 테이블 리스트/플로우 위젯에서 코드값 대신 라벨로 표시되도록 수정
  - InteractiveDataTable: categoryMappings 상태 추가 및 formatCellValue에서 카테고리 라벨 변환
  - FlowWidget: categoryMappings 상태 추가 및 formatValue에서 카테고리 라벨 변환
  - TableListComponent: categoryMappings 상태 추가 및 formatCellValue에서 카테고리 라벨 변환

- FlowWidget 런타임 에러 수정
  - formatValue 함수를 categoryMappings 상태 선언 이후로 이동
  - useCallback 의존성 배열 오류 해결

- Dialog 컴포넌트 빌드 오류 수정
  - CopyScreenModal: DialogFooter → ResizableDialogFooter 태그 일치
  - MenuAssignmentModal: 모든 Dialog 컴포넌트를 ResizableDialog 버전으로 변경
    - Dialog → ResizableDialog
    - DialogContent → ResizableDialogContent
    - DialogFooter → ResizableDialogFooter
    - DialogHeader → ResizableDialogHeader
    - DialogTitle → ResizableDialogTitle
    - DialogDescription → ResizableDialogDescription

- 불필요한 console.log 제거
  - InteractiveDataTable, FlowWidget, TableListComponent에서 디버깅 로그 정리
2025-11-06 12:03:28 +09:00
kjs 05192f6283 Merge pull request 'feature/screen-management' (#186) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/186
2025-11-06 11:58:22 +09:00
kjs e25f8893b0 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-06 11:58:09 +09:00
kjs ff2a069b79 fix: 테이블 리스트 불필요한 스크롤 및 하단 공간 문제 해결
문제:
- 고정 높이 (h-[400px] sm:h-[500px])로 인해 데이터가 적어도 큰 공간 차지
- 하단에 빈 공간이 남는데도 스크롤이 생기는 비효율적인 UX
- overflow-y-scroll이 항상 스크롤바를 표시함

해결:
- 고정 높이 제거 → flex-1 (부모의 남은 공간 차지)
- overflow-y-scroll → overflow-y-auto (필요할 때만 스크롤)
- 데이터 양에 따라 자동으로 높이 조정

개선 사항:
 데이터가 적을 때: 불필요한 공간 없이 컴팩트하게 표시
 데이터가 많을 때: 자동으로 스크롤 생성
 반응형 레이아웃에 자연스럽게 적응
 스크롤바가 필요할 때만 표시되어 깔끔한 UI
2025-11-06 11:53:59 +09:00
kjs 310f43e1bd fix: 테이블 그룹 헤더 스크롤 시 배경 비침 현상 수정
문제:
- 그룹 헤더의 bg-muted/50 (반투명 배경)으로 인해 스크롤 시 뒤 내용이 비쳐 보임
- sticky 위치에서 가독성 저하

해결:
- bg-muted/50 → bg-muted (불투명 배경)
- hover 효과도 hover:bg-muted → hover:bg-muted/80으로 조정
- 스크롤 시 깔끔한 가림 효과 제공

개선 사항:
- sticky 그룹 헤더의 완전한 배경 덮기
- 스크롤 시 가독성 향상
- shadcn 가이드라인 준수 (단색 배경)
2025-11-06 11:52:43 +09:00
kjs 4f02f0bad1 refactor: TableList 컴포넌트 그라데이션 제거 (shadcn 가이드라인 준수)
- 테이블 헤더의 그라데이션 제거 (bg-gradient-to-b from-muted/50 to-muted → bg-muted)
- CardModeRenderer 빈 상태 아이콘의 그라데이션 제거
- 하드코딩된 slate 색상을 shadcn 토큰으로 변경 (bg-muted, text-muted-foreground)
- 일관된 단색 배경으로 심플하고 깔끔한 디자인 유지

shadcn/ui 가이드라인:
- 테이블 헤더는 단색 bg-muted 사용
- 색상 토큰 사용으로 다크모드 자동 대응
- 불필요한 그라데이션 제거
2025-11-06 11:51:11 +09:00
kjs 2b2c096a99 refactor: ButtonPrimaryComponent를 shadcn 가이드라인에 맞게 수정
- 그라데이션 배경 제거하고 단색 배경 적용
- 동적 색상 기반 그림자 제거하고 표준 shadcn 그림자 적용
- hover:opacity-90 효과 추가 (부드러운 어두워짐)
- active:scale-95 효과 추가 (클릭 피드백)
- transition-colors duration-150으로 빠른 색상 전환 적용
- disabled 상태를 단색 회색으로 개선

shadcn/ui 가이드라인 준수:
- 심플하고 깔끔한 단색 디자인
- 일관된 인터랙션 패턴
- 표준화된 그림자 및 전환 효과
2025-11-06 11:49:24 +09:00
kjs fe306aed26 feat: 카테고리 위젯에 드래그 가능한 리사이저 추가
- 좌우 영역을 드래그로 조절 가능
- 리사이저: GripVertical 아이콘으로 시각적 표시
- 좌측 영역: 최소 10%, 최대 40%로 제한
- 호버 시 배경색 변경으로 피드백 제공
- 드래그 중 커서 및 텍스트 선택 방지
2025-11-06 11:40:59 +09:00
kjs 4b568f86b1 style: 카테고리 위젯 좌측 영역 더 축소
- 좌측 영역: 20% → 15%
- 우측 영역: 80% → 85%
- 최소한의 공간으로 컬럼 목록 표시
2025-11-06 11:40:19 +09:00
kjs 107ca3b0b8 style: 카테고리 위젯 좌우 비율 조정
- 좌측 영역: 30% → 20%
- 우측 영역: 70% → 80%
- 좌측은 컬럼 목록만 표시하므로 좁게 조정
- 우측 값 관리 영역에 더 많은 공간 확보
2025-11-06 11:39:21 +09:00
kjs 7efb31a367 feat: 카테고리 컬럼 카드에 항목 개수 표시
- 컬럼명(column_name) 제거
- 우측에 해당 카테고리의 항목 개수 표시
- getCategoryValues API로 각 컬럼의 값 개수 조회
- 'N개' 형식으로 깔끔하게 표시
- 로딩 중에는 '...' 표시
2025-11-06 11:36:45 +09:00
kjs 9f9e9ecd82 style: 카테고리 컬럼 카드 상하 패딩 8px로 조정
- CategoryColumnList 카드: p-4 → px-4 py-2
- 상하 여백 16px → 8px
- 좌우 여백은 16px 유지
- 채번규칙과 일관된 레이아웃
2025-11-06 11:34:08 +09:00
kjs ec2f544a3e style: 채번규칙 규칙명과 미리보기를 한 줄로 배치
- 규칙명과 미리보기를 flex로 나란히 배치
- 각각 flex-1로 동일한 너비 (50:50)
- gap-3로 간격 설정
- 공간 효율성 향상
2025-11-06 11:26:38 +09:00
kjs e964c04523 style: 채번규칙 미리보기 UI 간소화
- '미리보기' 제목 및 Card 컴포넌트 제거
- '코드 미리보기' 라벨 제거
- 한 줄로 간결하게 표현 (px-3 py-2)
- 불필요한 여백 제거로 깔끔한 레이아웃
2025-11-06 11:25:59 +09:00
kjs fc18523bb6 feat: 채번규칙 적용 범위 UI 제거 및 기본값 '메뉴 적용'으로 변경
- 적용 범위 선택 섹션 제거 (UI 간소화)
- 새 규칙 생성 시 scopeType 기본값: 'global' → 'menu'
- 모든 규칙이 자동으로 메뉴별 적용으로 생성됨
2025-11-06 11:23:27 +09:00
kjs 8fa068222e style: 채번규칙 카드에서 코드 미리보기 제거
- NumberingRulePreview 컴포넌트 제거
- CardContent 섹션 제거
- 규칙 이름과 삭제 버튼만 표시하는 심플한 레이아웃
2025-11-06 11:22:22 +09:00
kjs 654cc4575b style: 채번규칙 카드 상하 패딩 8px로 조정
- py-0 → py-2 (8px)
- 적절한 여백 유지하면서 컴팩트한 레이아웃
2025-11-06 11:21:02 +09:00
kjs 1ee2d8f365 style: 채번규칙 카드 자체의 상하 패딩 제거
- Card 컴포넌트에 py-0 추가
- 카드 내부 여백 최소화
2025-11-06 11:20:13 +09:00
kjs f7f410dbbe style: 채번규칙 카드 내부 상하 여백 완전 제거
- CardHeader, CardContent의 py를 0으로 설정
- 좌우 여백(px-3)만 유지
- 최대한 컴팩트한 카드 레이아웃
2025-11-06 10:44:08 +09:00
kjs 7132f4a90f style: 채번규칙 카드 내부 여백 축소
- CardHeader: py-3 → py-2 (12px → 8px)
- CardContent: py-3 → pb-2 (하단만 8px)
- 더 컴팩트한 카드 레이아웃
2025-11-06 10:42:55 +09:00
kjs 38734079e8 style: 채번규칙 카드 UI 개선
- '규칙 N개' 텍스트 제거 (불필요한 정보)
- 카드 내부 상하 여백 명시적으로 12px(py-3)로 설정
2025-11-06 10:41:01 +09:00
kjs 44def0979c fix: 화면 편집기 높이 입력 필드 1px 단위 조절 가능하도록 수정
- 문제: 높이 입력 시 10 단위로만 입력 가능 (예: 1080 입력 불가)
- 원인: 격자 스냅 로직이 onChange마다 높이를 10/20 단위로 강제 반올림
- 해결:
  1. 모든 number input 필드에 step="1" 추가
  2. ScreenDesigner.tsx의 격자 스냅 로직 수정 (높이 스냅 제거)
  3. UnifiedPropertiesPanel.tsx에 로컬 상태 추가하여 입력 중 스냅 방지
  4. onBlur/Enter 시에만 실제 값 업데이트

수정 파일:
- frontend/components/screen/ScreenDesigner.tsx
- frontend/components/screen/panels/UnifiedPropertiesPanel.tsx
- frontend/components/screen/panels/PropertiesPanel.tsx
- frontend/components/screen/panels/ResolutionPanel.tsx
- frontend/components/screen/panels/RowSettingsPanel.tsx
- frontend/components/screen/panels/webtype-configs/NumberTypeConfigPanel.tsx
- frontend/components/screen/panels/webtype-configs/TextTypeConfigPanel.tsx
2025-11-06 10:37:20 +09:00
kjs cf9e81a216 테이블에 카테고리 값 보이기 2025-11-05 18:28:43 +09:00
kjs 4c98839df8 코드 에러 수정 2025-11-05 18:13:06 +09:00
kjs ad46249c8b 카테고리 2025-11-05 18:09:16 +09:00
kjs bc029d1df8 카테고리 구현 2025-11-05 18:08:51 +09:00
hyeonsu dc6356671f Merge pull request 'feat/dashboard' (#185) from feat/dashboard into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/185
2025-11-05 16:54:24 +09:00
kjs f3bed0d713 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2025-11-05 16:38:30 +09:00
kjs fe1c99c727 카테고리 2025-11-05 15:24:05 +09:00
kjs 573a300a4a 카테고리 기능 구현 2025-11-05 15:23:57 +09:00
926 changed files with 283584 additions and 41541 deletions

View File

@ -0,0 +1,394 @@
# AI-개발자 협업 작업 수칙
## 핵심 원칙: "추측 금지, 확인 필수"
AI는 코드 작성 전에 반드시 실제 상황을 확인해야 합니다.
---
## 1. 데이터베이스 관련 작업
### 필수 확인 사항
- ✅ **항상 MCP Postgres로 실제 테이블 구조를 먼저 확인**
- ✅ 컬럼명, 데이터 타입, 제약조건을 추측하지 말고 쿼리로 확인
- ✅ 변경 후 실제로 데이터가 어떻게 보이는지 SELECT로 검증
### 확인 방법
```sql
-- 테이블 구조 확인
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_name = '테이블명'
ORDER BY ordinal_position;
-- 실제 데이터 확인
SELECT * FROM 테이블명 LIMIT 5;
```
### 금지 사항
- ❌ "아마도 `created_at` 컬럼일 것입니다" → 확인 필수!
- ❌ "보통 이렇게 되어있습니다" → 이 프로젝트에서 확인!
- ❌ 다른 테이블 구조를 보고 추측 → 각 테이블마다 확인!
---
## 2. 코드 수정 작업
### 작업 전
1. **관련 파일 읽기**: 수정할 파일의 현재 상태 확인
2. **의존성 파악**: 다른 파일에 영향이 있는지 검색
3. **기존 패턴 확인**: 프로젝트의 코딩 스타일 준수
### 작업 중
1. **한 번에 하나씩**: 하나의 명확한 작업만 수행
2. **로그 추가**: 디버깅이 필요하면 명확한 로그 추가
3. **점진적 수정**: 큰 변경은 여러 단계로 나눔
### 작업 후
1. **로그 제거**: 디버깅 로그는 반드시 제거
2. **테스트 제안**: 브라우저로 테스트할 것을 제안
3. **변경사항 요약**: 무엇을 어떻게 바꿨는지 명확히 설명
---
## 3. 확인 및 검증
### 확인 도구 사용
- **MCP Postgres**: 데이터베이스 구조 및 데이터 확인
- **MCP Browser**: 실제 화면에서 동작 확인
- **codebase_search**: 관련 코드 패턴 검색
- **grep**: 특정 문자열 사용처 찾기
### 검증 프로세스
1. **변경 전 상태 확인** → 문제 파악
2. **변경 적용**
3. **변경 후 상태 확인** → 해결 검증
4. **부작용 확인** → 다른 기능에 영향 없는지
### 사용자 피드백 대응
- 사용자가 "확인 안하지?"라고 하면:
1. 즉시 사과
2. MCP/브라우저로 실제 확인
3. 정확한 정보를 바탕으로 재작업
---
## 4. 커뮤니케이션
### 작업 시작 시
```
✅ 좋은 예:
"MCP로 item_info 테이블 구조를 먼저 확인하겠습니다."
❌ 나쁜 예:
"보통 created_at 컬럼이 있을 것이므로 수정하겠습니다."
```
### 작업 완료 시
```
✅ 좋은 예:
"완료! 두 가지를 수정했습니다:
1. 기본 높이를 40px → 30px로 변경 (ScreenDesigner.tsx:2174)
2. 숨김 컬럼을 created_date, updated_date, writer, company_code로 수정 (TablesPanel.tsx:57)
테스트해보세요!"
❌ 나쁜 예:
"수정했습니다!"
```
### 불확실할 때
```
✅ 좋은 예:
"컬럼명이 created_at인지 created_date인지 확실하지 않습니다.
MCP로 확인해도 될까요?"
❌ 나쁜 예:
"created_at일 것 같으니 일단 이렇게 하겠습니다."
```
---
## 5. 금지 사항
### 절대 금지
1. ❌ **확인 없이 "완료했습니다" 말하기**
- 반드시 실제로 확인하고 보고
2. ❌ **이전에 실패한 방법 반복하기**
- 같은 실수를 두 번 하지 않기
3. ❌ **디버깅 로그를 남겨둔 채 작업 종료**
- 모든 console.log 제거 확인
4. ❌ **추측으로 답변하기**
- "아마도", "보통", "일반적으로" 금지
- 확실하지 않으면 먼저 확인
5. ❌ **여러 문제를 한 번에 수정하려고 시도**
- 한 번에 하나씩 해결
---
## 6. 프로젝트 특별 규칙
### 백엔드 관련
- 🔥 **백엔드 재시작 절대 금지** (사용자 명시 규칙)
- 🔥 Node.js 프로세스를 건드리지 않음
### 데이터베이스 관련
- 🔥 **멀티테넌시 규칙 준수**
- 모든 쿼리에 `company_code` 필터링 필수
- `company_code = "*"`는 최고 관리자 전용
- 자세한 내용: `.cursor/rules/multi-tenancy-guide.mdc`
### API 관련
- 🔥 **API 클라이언트 사용 필수**
- `fetch()` 직접 사용 금지
- `lib/api/` 의 클라이언트 함수 사용
- 환경별 URL 자동 처리
### UI 관련
- 🔥 **shadcn/ui 스타일 가이드 준수**
- CSS 변수 사용 (하드코딩 금지)
- 중첩 박스 금지 (명시 요청 전까지)
- 이모지 사용 금지 (명시 요청 전까지)
---
## 7. 에러 처리
### 에러 발생 시 프로세스
1. **에러 로그 전체 읽기**
- 스택 트레이스 확인
- 에러 메시지 정확히 파악
2. **근본 원인 파악**
- 증상이 아닌 원인 찾기
- 왜 이 에러가 발생했는지 이해
3. **해결책 적용**
- 임시방편이 아닌 근본적 해결
- 같은 에러가 재발하지 않도록
4. **검증**
- 실제로 에러가 해결되었는지 확인
- 다른 부작용은 없는지 확인
### 에러 로깅
```typescript
// ✅ 좋은 로그 (디버깅 시)
console.log("🔍 [컴포넌트명] 작업명:", {
관련변수1,
관련변수2,
예상결과,
});
// ❌ 나쁜 로그
console.log("here");
console.log(data); // 무슨 데이터인지 알 수 없음
```
---
## 8. 작업 완료 체크리스트
모든 작업 완료 전에 다음을 확인:
- [ ] 실제 데이터베이스/파일을 확인했는가?
- [ ] 변경사항이 의도대로 작동하는가?
- [ ] 디버깅 로그를 모두 제거했는가?
- [ ] 다른 기능에 부작용이 없는가?
- [ ] 멀티테넌시 규칙을 준수했는가?
- [ ] 사용자에게 명확히 설명했는가?
---
## 9. 모범 사례
### 데이터베이스 확인 예시
```typescript
// 1. MCP로 테이블 구조 확인
mcp_postgres_query: SELECT column_name FROM information_schema.columns
WHERE table_name = 'item_info';
// 2. 실제 컬럼명 확인 후 코드 작성
const hiddenColumns = new Set([
'id',
'created_date', // ✅ 실제 확인한 컬럼명
'updated_date', // ✅ 실제 확인한 컬럼명
'writer', // ✅ 실제 확인한 컬럼명
'company_code'
]);
```
### 브라우저 테스트 제안 예시
```
"수정이 완료되었습니다!
다음을 테스트해주세요:
1. 화면관리 > 테이블 탭 열기
2. item_info 테이블 확인
3. 기본 5개 컬럼(id, created_date 등)이 안 보이는지 확인
4. 새 컬럼 드래그앤드롭 시 높이가 30px인지 확인
브라우저 테스트를 원하시면 말씀해주세요!"
```
---
## 10. 요약: 핵심 3원칙
1. **확인 우선** 🔍
- 추측하지 말고, 항상 확인하고 작업
2. **한 번에 하나** 🎯
- 여러 문제를 동시에 해결하려 하지 말기
3. **철저한 마무리** ✨
- 로그 제거, 테스트, 명확한 설명
---
## 11. 화면관리 시스템 위젯 개발 가이드
### 위젯 크기 설정의 핵심 원칙
화면관리 시스템에서 위젯을 개발할 때, **크기 제어는 상위 컨테이너(`RealtimePreviewDynamic`)가 담당**합니다.
#### ✅ 올바른 크기 설정 패턴
```tsx
// 위젯 컴포넌트 내부
export function YourWidget({ component }: YourWidgetProps) {
return (
<div
className="flex h-full w-full items-center justify-between gap-2"
style={{
padding: component.style?.padding || "0.75rem",
backgroundColor: component.style?.backgroundColor,
// ❌ width, height, minHeight 등 크기 관련 속성은 제거!
}}
>
{/* 위젯 내용 */}
</div>
);
}
```
#### ❌ 잘못된 크기 설정 패턴
```tsx
// 이렇게 하면 안 됩니다!
<div
style={{
width: component.style?.width || "100%", // ❌ 상위에서 이미 제어함
height: component.style?.height || "80px", // ❌ 상위에서 이미 제어함
minHeight: "80px", // ❌ 내부 컨텐츠가 줄어듦
}}
>
```
### 이유
1. **`RealtimePreviewDynamic`**이 `baseStyle`로 이미 크기를 제어:
```tsx
const baseStyle = {
left: `${position.x}px`,
top: `${position.y}px`,
width: getWidth(), // size.width 사용
height: getHeight(), // size.height 사용
};
```
2. 위젯 내부에서 크기를 다시 설정하면:
- 중복 설정으로 인한 충돌
- 내부 컨텐츠가 설정한 크기보다 작게 표시됨
- 편집기에서 설정한 크기와 실제 렌더링 크기 불일치
### 위젯이 관리해야 할 스타일
위젯 컴포넌트는 **위젯 고유의 스타일**만 관리합니다:
- ✅ `padding`: 내부 여백
- ✅ `backgroundColor`: 배경색
- ✅ `border`, `borderRadius`: 테두리
- ✅ `gap`: 자식 요소 간격
- ✅ `flexDirection`, `alignItems`: 레이아웃 방향
### 위젯 등록 시 defaultSize
```tsx
ComponentRegistry.registerComponent({
id: "your-widget",
name: "위젯 이름",
category: "utility",
defaultSize: { width: 1200, height: 80 }, // 픽셀 단위 (필수)
component: YourWidget,
defaultProps: {
style: {
padding: "0.75rem",
// width, height는 defaultSize로 제어되므로 여기 불필요
},
},
});
```
### 레이아웃 구조
```tsx
// 전체 높이를 차지하고 내부 요소를 정렬
<div className="flex h-full w-full items-center justify-between gap-2">
{/* 왼쪽 컨텐츠 */}
<div className="flex items-center gap-3">{/* ... */}</div>
{/* 오른쪽 버튼들 */}
<div className="flex items-center gap-2 flex-shrink-0">
{/* flex-shrink-0으로 버튼이 줄어들지 않도록 보장 */}
</div>
</div>
```
### 체크리스트
위젯 개발 시 다음을 확인하세요:
- [ ] 위젯 루트 요소에 `h-full w-full` 클래스 사용
- [ ] `width`, `height`, `minHeight` 인라인 스타일 **제거**
- [ ] `padding`, `backgroundColor` 등 위젯 고유 스타일만 관리
- [ ] `defaultSize`에 적절한 기본 크기 설정
- [ ] 양끝 정렬이 필요하면 `justify-between` 사용
- [ ] 줄어들면 안 되는 요소에 `flex-shrink-0` 적용
---
**이 규칙을 지키지 않으면 사용자에게 "확인 안하지?"라는 말을 듣게 됩니다!**

View File

@ -0,0 +1,279 @@
# inputType 사용 가이드
## 핵심 원칙
**컬럼 타입 판단 시 반드시 `inputType`을 사용해야 합니다. `webType`은 레거시이며 더 이상 사용하지 않습니다.**
---
## 올바른 사용법
### ✅ inputType 사용 (권장)
```typescript
// 카테고리 타입 체크
if (columnMeta.inputType === "category") {
// 카테고리 처리 로직
}
// 코드 타입 체크
if (meta.inputType === "code") {
// 코드 처리 로직
}
// 필터링
const categoryColumns = Object.entries(columnMeta)
.filter(([_, meta]) => meta.inputType === "category")
.map(([columnName, _]) => columnName);
```
### ❌ webType 사용 (금지)
```typescript
// ❌ 절대 사용 금지!
if (columnMeta.webType === "category") { ... }
// ❌ 이것도 금지!
const categoryColumns = columns.filter(col => col.webType === "category");
```
---
## API에서 inputType 가져오기
### Backend API
```typescript
// 컬럼 입력 타입 정보 가져오기
const inputTypes = await tableTypeApi.getColumnInputTypes(tableName);
// inputType 맵 생성
const inputTypeMap: Record<string, string> = {};
inputTypes.forEach((col: any) => {
inputTypeMap[col.columnName] = col.inputType;
});
```
### columnMeta 구조
```typescript
interface ColumnMeta {
webType?: string; // 레거시, 사용 금지
codeCategory?: string;
inputType?: string; // ✅ 반드시 이것 사용!
}
const columnMeta: Record<string, ColumnMeta> = {
material: {
webType: "category", // 무시
codeCategory: "",
inputType: "category", // ✅ 이것만 사용
},
};
```
---
## 캐시 사용 시 주의사항
### ❌ 잘못된 캐시 처리 (inputType 누락)
```typescript
const cached = tableColumnCache.get(cacheKey);
if (cached) {
const meta: Record<string, ColumnMeta> = {};
cached.columns.forEach((col: any) => {
meta[col.columnName] = {
webType: col.webType,
codeCategory: col.codeCategory,
// ❌ inputType 누락!
};
});
}
```
### ✅ 올바른 캐시 처리 (inputType 포함)
```typescript
const cached = tableColumnCache.get(cacheKey);
if (cached) {
const meta: Record<string, ColumnMeta> = {};
// 캐시된 inputTypes 맵 생성
const inputTypeMap: Record<string, string> = {};
if (cached.inputTypes) {
cached.inputTypes.forEach((col: any) => {
inputTypeMap[col.columnName] = col.inputType;
});
}
cached.columns.forEach((col: any) => {
meta[col.columnName] = {
webType: col.webType,
codeCategory: col.codeCategory,
inputType: inputTypeMap[col.columnName], // ✅ inputType 포함!
};
});
}
```
---
## 주요 inputType 종류
| inputType | 설명 | 사용 예시 |
| ---------- | ---------------- | ------------------ |
| `text` | 일반 텍스트 입력 | 이름, 설명 등 |
| `number` | 숫자 입력 | 금액, 수량 등 |
| `date` | 날짜 입력 | 생성일, 수정일 등 |
| `datetime` | 날짜+시간 입력 | 타임스탬프 등 |
| `category` | 카테고리 선택 | 분류, 상태 등 |
| `code` | 공통 코드 선택 | 코드 마스터 데이터 |
| `boolean` | 예/아니오 | 활성화 여부 등 |
| `email` | 이메일 입력 | 이메일 주소 |
| `url` | URL 입력 | 웹사이트 주소 |
| `image` | 이미지 업로드 | 프로필 사진 등 |
| `file` | 파일 업로드 | 첨부파일 등 |
---
## 실제 적용 사례
### 1. TableListComponent - 카테고리 매핑 로드
```typescript
// ✅ inputType으로 카테고리 컬럼 필터링
const categoryColumns = Object.entries(columnMeta)
.filter(([_, meta]) => meta.inputType === "category")
.map(([columnName, _]) => columnName);
// 각 카테고리 컬럼의 값 목록 조회
for (const columnName of categoryColumns) {
const response = await apiClient.get(
`/table-categories/${tableName}/${columnName}/values`
);
// 매핑 처리...
}
```
### 2. InteractiveDataTable - 셀 값 렌더링
```typescript
// ✅ inputType으로 렌더링 분기
const inputType = columnMeta[column.columnName]?.inputType;
switch (inputType) {
case "category":
// 카테고리 배지 렌더링
return <Badge>{categoryLabel}</Badge>;
case "code":
// 코드명 표시
return codeName;
case "date":
// 날짜 포맷팅
return formatDate(value);
default:
return value;
}
```
### 3. 검색 필터 생성
```typescript
// ✅ inputType에 따라 다른 검색 UI 제공
const renderSearchInput = (column: ColumnConfig) => {
const inputType = columnMeta[column.columnName]?.inputType;
switch (inputType) {
case "category":
return <CategorySelect column={column} />;
case "code":
return <CodeSelect column={column} />;
case "date":
return <DateRangePicker column={column} />;
case "number":
return <NumberRangeInput column={column} />;
default:
return <TextInput column={column} />;
}
};
```
---
## 마이그레이션 체크리스트
기존 코드에서 `webType`을 `inputType`으로 전환할 때:
- [ ] `webType` 참조를 모두 `inputType`으로 변경
- [ ] API 호출 시 `getColumnInputTypes()` 포함 확인
- [ ] 캐시 사용 시 `cached.inputTypes` 매핑 확인
- [ ] 타입 정의에서 `inputType` 필드 포함
- [ ] 조건문에서 `inputType` 체크로 변경
- [ ] 테스트 실행하여 정상 동작 확인
---
## 디버깅 팁
### inputType이 undefined인 경우
```typescript
// 디버깅 로그 추가
console.log("columnMeta:", columnMeta);
console.log("inputType:", columnMeta[columnName]?.inputType);
// 체크 포인트:
// 1. getColumnInputTypes() 호출 확인
// 2. inputTypeMap 생성 확인
// 3. meta 객체에 inputType 할당 확인
// 4. 캐시 사용 시 cached.inputTypes 확인
```
### webType만 있고 inputType이 없는 경우
```typescript
// ❌ 잘못된 데이터 구조
{
material: {
webType: "category",
codeCategory: "",
// inputType 누락!
}
}
// ✅ 올바른 데이터 구조
{
material: {
webType: "category", // 레거시, 무시됨
codeCategory: "",
inputType: "category" // ✅ 필수!
}
}
```
---
## 참고 자료
- **컴포넌트**: `/frontend/lib/registry/components/table-list/TableListComponent.tsx`
- **API 클라이언트**: `/frontend/lib/api/tableType.ts`
- **타입 정의**: `/frontend/types/table.ts`
---
## 요약
1. **항상 `inputType` 사용**, `webType` 사용 금지
2. **API에서 `getColumnInputTypes()` 호출** 필수
3. **캐시 사용 시 `inputTypes` 포함** 확인
4. **디버깅 시 `inputType` 값 확인**
5. **기존 코드 마이그레이션** 시 체크리스트 활용

View File

@ -0,0 +1,844 @@
---
priority: critical
applies_to: all
check_frequency: always
enforcement: mandatory
---
# 멀티테넌시(Multi-Tenancy) 필수 구현 가이드
**🚨 최우선 보안 규칙: 이 문서의 모든 규칙은 예외 없이 반드시 준수해야 합니다.**
**⚠️ AI 에이전트는 모든 코드 작성/수정 후 반드시 이 체크리스트를 확인해야 합니다.**
## 핵심 원칙
**모든 비즈니스 데이터는 회사별(company_code)로 완벽하게 격리되어야 합니다.**
이 시스템은 멀티테넌트 아키텍처를 사용하며, 각 회사(tenant)는 자신의 데이터만 접근할 수 있어야 합니다.
다른 회사의 데이터에 접근하는 것은 **치명적인 보안 취약점**입니다.
---
## 1. 데이터베이스 스키마 요구사항
### 1.1 company_code 컬럼 필수
**모든 비즈니스 테이블은 `company_code` 컬럼을 반드시 포함해야 합니다.**
```sql
CREATE TABLE example_table (
id SERIAL PRIMARY KEY,
company_code VARCHAR(20) NOT NULL, -- ✅ 필수!
name VARCHAR(100),
created_at TIMESTAMPTZ DEFAULT NOW(),
-- 외래키 제약조건 (필수)
CONSTRAINT fk_company FOREIGN KEY (company_code)
REFERENCES company_mng(company_code)
ON DELETE CASCADE ON UPDATE CASCADE
);
-- 성능을 위한 인덱스 (필수)
CREATE INDEX idx_example_company_code ON example_table(company_code);
-- 복합 유니크 제약조건 (중복 방지)
CREATE UNIQUE INDEX idx_example_unique
ON example_table(name, company_code); -- 회사별로 고유해야 하는 경우
```
### 1.2 예외 테이블 (company_code 불필요)
**⚠️ 유일한 예외: `company_mng` 테이블만 `company_code`가 없습니다.**
이 테이블은 회사 정보를 저장하는 마스터 테이블이므로 예외입니다.
**모든 다른 테이블은 예외 없이 `company_code`가 필수입니다:**
- ✅ `user_info` → `company_code` 필수 (사용자는 특정 회사 소속)
- ✅ `menu_info` → `company_code` 필수 (회사별 메뉴 설정 가능)
- ✅ `system_config` → `company_code` 필수 (회사별 시스템 설정)
- ✅ `audit_log` → `company_code` 필수 (회사별 감사 로그)
- ✅ 모든 비즈니스 테이블 → `company_code` 필수
**새로운 테이블 생성 시 체크리스트:**
- [ ] `company_mng` 테이블인가? → `company_code` 불필요 (유일한 예외)
- [ ] 그 외 모든 테이블 → `company_code` 필수 (예외 없음)
- [ ] `company_code` 없이 테이블을 만들려고 하는가? → 다시 생각하세요!
---
## 2. 백엔드 API 구현 필수 사항
### 2.1 모든 데이터 조회 시 필터링
**절대 원칙: 모든 SELECT 쿼리는 company_code 필터링을 반드시 포함해야 합니다.**
#### ✅ 올바른 방법
```typescript
async function getDataList(req: Request, res: Response) {
const companyCode = req.user!.companyCode; // 인증된 사용자의 회사 코드
let query: string;
let params: any[];
if (companyCode === "*") {
// 최고 관리자: 모든 회사 데이터 조회 가능
query = `
SELECT * FROM example_table
ORDER BY created_at DESC
`;
params = [];
logger.info("최고 관리자 전체 데이터 조회");
} else {
// 일반 회사: 자신의 회사 데이터만 조회
query = `
SELECT * FROM example_table
WHERE company_code = $1
ORDER BY created_at DESC
`;
params = [companyCode];
logger.info("회사별 데이터 조회", { companyCode });
}
const result = await pool.query(query, params);
return res.json({ success: true, data: result.rows });
}
```
#### ❌ 잘못된 방법 - 절대 사용 금지
```typescript
// 🚨 치명적 보안 취약점: company_code 필터링 없음
async function getDataList(req: Request, res: Response) {
const query = `SELECT * FROM example_table`; // 모든 회사 데이터 노출!
const result = await pool.query(query);
return res.json({ success: true, data: result.rows });
}
```
### 2.2 데이터 생성 (INSERT)
**모든 INSERT 쿼리는 company_code를 반드시 포함해야 합니다.**
#### ✅ 올바른 방법
```typescript
async function createData(req: Request, res: Response) {
const companyCode = req.user!.companyCode; // 서버에서 확정
const { name, description } = req.body;
const query = `
INSERT INTO example_table (company_code, name, description)
VALUES ($1, $2, $3)
RETURNING *
`;
const result = await pool.query(query, [companyCode, name, description]);
logger.info("데이터 생성", {
companyCode,
id: result.rows[0].id,
});
return res.json({ success: true, data: result.rows[0] });
}
```
#### ❌ 클라이언트 입력 사용 금지
```typescript
// 🚨 보안 취약점: 클라이언트가 임의의 회사 코드 지정 가능
async function createData(req: Request, res: Response) {
const { companyCode, name } = req.body; // 사용자가 다른 회사 코드 전달 가능!
const query = `INSERT INTO example_table (company_code, name) VALUES ($1, $2)`;
await pool.query(query, [companyCode, name]);
}
```
### 2.3 데이터 수정 (UPDATE)
**WHERE 절에 company_code를 반드시 포함해야 합니다.**
#### ✅ 올바른 방법
```typescript
async function updateData(req: Request, res: Response) {
const companyCode = req.user!.companyCode;
const { id } = req.params;
const { name, description } = req.body;
let query: string;
let params: any[];
if (companyCode === "*") {
// 최고 관리자: 모든 데이터 수정 가능
query = `
UPDATE example_table
SET name = $1, description = $2, updated_at = NOW()
WHERE id = $3
RETURNING *
`;
params = [name, description, id];
} else {
// 일반 회사: 자신의 데이터만 수정 가능
query = `
UPDATE example_table
SET name = $1, description = $2, updated_at = NOW()
WHERE id = $3 AND company_code = $4
RETURNING *
`;
params = [name, description, id, companyCode];
}
const result = await pool.query(query, params);
if (result.rowCount === 0) {
return res.status(404).json({
success: false,
message: "데이터를 찾을 수 없거나 권한이 없습니다",
});
}
logger.info("데이터 수정", { companyCode, id });
return res.json({ success: true, data: result.rows[0] });
}
```
#### ❌ 잘못된 방법
```typescript
// 🚨 보안 취약점: 다른 회사의 같은 ID 데이터도 수정됨
const query = `
UPDATE example_table
SET name = $1, description = $2
WHERE id = $3
`;
```
### 2.4 데이터 삭제 (DELETE)
**WHERE 절에 company_code를 반드시 포함해야 합니다.**
#### ✅ 올바른 방법
```typescript
async function deleteData(req: Request, res: Response) {
const companyCode = req.user!.companyCode;
const { id } = req.params;
let query: string;
let params: any[];
if (companyCode === "*") {
// 최고 관리자: 모든 데이터 삭제 가능
query = `DELETE FROM example_table WHERE id = $1 RETURNING id`;
params = [id];
} else {
// 일반 회사: 자신의 데이터만 삭제 가능
query = `
DELETE FROM example_table
WHERE id = $1 AND company_code = $2
RETURNING id
`;
params = [id, companyCode];
}
const result = await pool.query(query, params);
if (result.rowCount === 0) {
return res.status(404).json({
success: false,
message: "데이터를 찾을 수 없거나 권한이 없습니다",
});
}
logger.info("데이터 삭제", { companyCode, id });
return res.json({ success: true });
}
```
---
## 3. company_code = "\*" 의 의미
### 3.1 최고 관리자 전용 데이터
**중요**: `company_code = "*"`는 **최고 관리자 전용 데이터**를 의미합니다.
- ❌ 잘못된 이해: `company_code = "*"` = 모든 회사가 공유하는 공통 데이터
- ✅ 올바른 이해: `company_code = "*"` = 최고 관리자만 관리하는 전용 데이터
### 3.2 데이터 격리 원칙
**회사별 데이터 접근 규칙:**
| 사용자 유형 | company_code | 접근 가능한 데이터 |
| ----------- | ------------ | ---------------------------------------------- |
| 회사 A | `COMPANY_A` | `company_code = 'COMPANY_A'` 데이터만 |
| 회사 B | `COMPANY_B` | `company_code = 'COMPANY_B'` 데이터만 |
| 최고 관리자 | `*` | 모든 회사 데이터 + `company_code = '*'` 데이터 |
**핵심**:
- 일반 회사는 `company_code = "*"` 데이터를 **절대 볼 수 없음**
- 일반 회사는 다른 회사의 데이터를 **절대 볼 수 없음**
- 최고 관리자만 모든 데이터에 접근 가능
---
## 4. 복잡한 쿼리에서의 멀티테넌시
### 4.1 JOIN 쿼리
**모든 JOIN된 테이블에도 company_code 필터링을 적용해야 합니다.**
#### ✅ 올바른 방법
```typescript
const query = `
SELECT
a.*,
b.name as category_name,
c.name as user_name
FROM example_table a
LEFT JOIN category_table b
ON a.category_id = b.id
AND a.company_code = b.company_code -- ✅ JOIN 조건에도 company_code 필수
LEFT JOIN user_info c
ON a.user_id = c.user_id
AND a.company_code = c.company_code
WHERE a.company_code = $1
`;
```
#### ❌ 잘못된 방법
```typescript
// 🚨 보안 취약점: JOIN에서 다른 회사 데이터와 섞임
const query = `
SELECT
a.*,
b.name as category_name
FROM example_table a
LEFT JOIN category_table b ON a.category_id = b.id -- company_code 없음!
WHERE a.company_code = $1
`;
```
### 4.2 서브쿼리
**모든 서브쿼리에도 company_code 필터링을 적용해야 합니다.**
#### ✅ 올바른 방법
```typescript
const query = `
SELECT * FROM example_table
WHERE category_id IN (
SELECT id FROM category_table
WHERE active = true AND company_code = $1 -- ✅
)
AND company_code = $1
`;
```
#### ❌ 잘못된 방법
```typescript
// 🚨 보안 취약점: 서브쿼리에서 company_code 누락
const query = `
SELECT * FROM example_table
WHERE category_id IN (
SELECT id FROM category_table WHERE active = true -- company_code 없음!
)
AND company_code = $1
`;
```
### 4.3 집계 함수 (COUNT, SUM 등)
**집계 함수도 company_code로 필터링해야 합니다.**
#### ✅ 올바른 방법
```typescript
const query = `
SELECT COUNT(*) as total
FROM example_table
WHERE company_code = $1
`;
```
#### ❌ 잘못된 방법
```typescript
// 🚨 보안 취약점: 모든 회사의 총합 반환
const query = `SELECT COUNT(*) as total FROM example_table`;
```
### 4.4 EXISTS 서브쿼리
```typescript
// ✅ 올바른 방법
const query = `
SELECT * FROM example_table a
WHERE EXISTS (
SELECT 1 FROM related_table b
WHERE b.example_id = a.id
AND b.company_code = a.company_code -- ✅ 필수
)
AND a.company_code = $1
`;
```
---
## 5. 자동 필터 시스템 (autoFilter)
### 5.1 백엔드 구현 (이미 완료)
백엔드에는 `autoFilter` 기능이 구현되어 있습니다:
```typescript
// tableManagementController.ts
let enhancedSearch = { ...search };
if (autoFilter?.enabled && req.user) {
const filterColumn = autoFilter.filterColumn || "company_code";
const userField = autoFilter.userField || "companyCode";
const userValue = (req.user as any)[userField];
if (userValue) {
enhancedSearch[filterColumn] = userValue;
logger.info("🔍 현재 사용자 필터 적용:", {
filterColumn,
userValue,
tableName,
});
}
}
```
### 5.2 프론트엔드 사용 (필수)
**모든 테이블 데이터 API 호출 시 `autoFilter`를 반드시 전달해야 합니다.**
#### ✅ 올바른 방법
```typescript
// frontend/lib/api/screen.ts
const requestBody = {
...params,
autoFilter: {
enabled: true,
filterColumn: "company_code",
userField: "companyCode",
},
};
const response = await apiClient.post(
`/table-management/tables/${tableName}/data`,
requestBody
);
```
#### Entity 조인 API
```typescript
// frontend/lib/api/entityJoin.ts
const autoFilter = {
enabled: true,
filterColumn: "company_code",
userField: "companyCode",
};
const response = await apiClient.get(
`/table-management/tables/${tableName}/data-with-joins`,
{
params: {
...params,
autoFilter: JSON.stringify(autoFilter),
},
}
);
```
---
## 6. 서비스 계층 패턴
### 6.1 표준 서비스 함수 패턴
**서비스 함수는 항상 companyCode를 첫 번째 파라미터로 받아야 합니다.**
```typescript
class ExampleService {
async findAll(companyCode: string, filters?: any) {
let query: string;
let params: any[];
if (companyCode === "*") {
query = `SELECT * FROM example_table`;
params = [];
} else {
query = `SELECT * FROM example_table WHERE company_code = $1`;
params = [companyCode];
}
return await pool.query(query, params);
}
async findById(companyCode: string, id: number) {
let query: string;
let params: any[];
if (companyCode === "*") {
query = `SELECT * FROM example_table WHERE id = $1`;
params = [id];
} else {
query = `SELECT * FROM example_table WHERE id = $1 AND company_code = $2`;
params = [id, companyCode];
}
const result = await pool.query(query, params);
return result.rows[0];
}
async create(companyCode: string, data: any) {
const query = `
INSERT INTO example_table (company_code, name, description)
VALUES ($1, $2, $3)
RETURNING *
`;
const result = await pool.query(query, [
companyCode,
data.name,
data.description,
]);
return result.rows[0];
}
}
// 컨트롤러에서 사용
const exampleService = new ExampleService();
async function getDataList(req: Request, res: Response) {
const companyCode = req.user!.companyCode;
const data = await exampleService.findAll(companyCode, req.query);
return res.json({ success: true, data });
}
```
---
## 7. 마이그레이션 체크리스트
### 7.1 새로운 테이블 생성 시
- [ ] `company_code VARCHAR(20) NOT NULL` 컬럼 추가
- [ ] `company_mng` 테이블에 대한 외래키 제약조건 추가
- [ ] `company_code`에 인덱스 생성
- [ ] 복합 유니크 제약조건에 `company_code` 포함
- [ ] 샘플 데이터에 올바른 `company_code` 값 포함
### 7.2 기존 테이블 마이그레이션 시
```sql
-- 1. company_code 컬럼 추가
ALTER TABLE example_table ADD COLUMN company_code VARCHAR(20);
-- 2. 기존 데이터를 모든 회사별로 복제
INSERT INTO example_table (company_code, name, description, created_at)
SELECT ci.company_code, et.name, et.description, et.created_at
FROM (SELECT * FROM example_table WHERE company_code IS NULL) et
CROSS JOIN company_mng ci
WHERE NOT EXISTS (
SELECT 1 FROM example_table et2
WHERE et2.name = et.name
AND et2.company_code = ci.company_code
);
-- 3. NULL 데이터 삭제
DELETE FROM example_table WHERE company_code IS NULL;
-- 4. NOT NULL 제약조건
ALTER TABLE example_table ALTER COLUMN company_code SET NOT NULL;
-- 5. 인덱스 및 외래키
CREATE INDEX idx_example_company ON example_table(company_code);
ALTER TABLE example_table
ADD CONSTRAINT fk_example_company
FOREIGN KEY (company_code) REFERENCES company_mng(company_code)
ON DELETE CASCADE ON UPDATE CASCADE;
```
---
## 8. 테스트 체크리스트
### 8.1 필수 테스트 시나리오
**모든 새로운 API는 다음 테스트를 통과해야 합니다:**
- [ ] **회사 A 테스트**: 회사 A로 로그인하여 회사 A 데이터만 보이는지 확인
- [ ] **회사 B 테스트**: 회사 B로 로그인하여 회사 B 데이터만 보이는지 확인
- [ ] **격리 테스트**: 회사 A로 로그인하여 회사 B 데이터에 접근 불가능한지 확인
- [ ] **최고 관리자 테스트**: 최고 관리자로 로그인하여 모든 데이터가 보이는지 확인
- [ ] **수정 권한 테스트**: 회사 A가 회사 B의 데이터를 수정할 수 없는지 확인
- [ ] **삭제 권한 테스트**: 회사 A가 회사 B의 데이터를 삭제할 수 없는지 확인
### 8.2 SQL 인젝션 테스트
```typescript
// company_code를 URL 파라미터로 전달하려는 시도 차단
// ❌ 이런 요청을 받아서는 안 됨
GET /api/data?company_code=COMPANY_B
// ✅ company_code는 항상 req.user에서 가져와야 함
const companyCode = req.user!.companyCode;
```
---
## 9. 감사 로그 (Audit Log)
### 9.1 모든 중요 작업에 로깅
```typescript
logger.info("데이터 생성", {
companyCode: req.user!.companyCode,
userId: req.user!.userId,
tableName: "example_table",
action: "INSERT",
recordId: result.rows[0].id,
});
logger.warn("권한 없는 접근 시도", {
companyCode: req.user!.companyCode,
userId: req.user!.userId,
attemptedRecordId: req.params.id,
message: "다른 회사의 데이터 접근 시도",
});
```
### 9.2 감사 로그 테이블 구조
```sql
CREATE TABLE audit_log (
id SERIAL PRIMARY KEY,
company_code VARCHAR(20) NOT NULL,
user_id VARCHAR(100) NOT NULL,
action VARCHAR(50) NOT NULL,
table_name VARCHAR(100),
record_id VARCHAR(100),
old_value JSONB,
new_value JSONB,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_audit_company ON audit_log(company_code);
CREATE INDEX idx_audit_action ON audit_log(action, created_at);
```
---
## 10. 보안 체크리스트 (코드 리뷰 시 필수)
### 10.1 백엔드 API 체크리스트
- [ ] 모든 SELECT 쿼리에 `WHERE company_code = $1` 포함 (최고 관리자 예외)
- [ ] 모든 INSERT 쿼리에 `company_code` 컬럼 포함
- [ ] 모든 UPDATE/DELETE 쿼리의 WHERE 절에 `company_code` 조건 포함
- [ ] JOIN 쿼리의 ON 절에 `company_code` 매칭 조건 포함
- [ ] 서브쿼리에 `company_code` 필터링 포함
- [ ] 집계 함수에 `company_code` 필터링 포함
- [ ] `req.user.companyCode` 사용 (클라이언트 입력 사용 금지)
- [ ] 최고 관리자(`company_code = "*"`) 예외 처리
- [ ] 로그에 `companyCode` 정보 포함
- [ ] 권한 없음 시 404 또는 403 반환
### 10.2 프론트엔드 체크리스트
- [ ] 모든 테이블 데이터 API 호출 시 `autoFilter` 전달
- [ ] `company_code`를 직접 전달하지 않음 (백엔드에서 자동 처리)
- [ ] 에러 발생 시 적절한 메시지 표시
### 10.3 데이터베이스 체크리스트
- [ ] 모든 비즈니스 테이블에 `company_code` 컬럼 존재
- [ ] `company_code`에 NOT NULL 제약조건 적용
- [ ] `company_code`에 인덱스 생성
- [ ] 외래키 제약조건으로 `company_mng` 참조
- [ ] 복합 유니크 제약조건에 `company_code` 포함
---
## 11. 일반적인 실수와 해결방법
### 실수 1: 서브쿼리에서 company_code 누락
```typescript
// ❌ 잘못된 방법
const query = `
SELECT * FROM example_table
WHERE category_id IN (
SELECT id FROM category_table WHERE active = true
)
AND company_code = $1
`;
// ✅ 올바른 방법
const query = `
SELECT * FROM example_table
WHERE category_id IN (
SELECT id FROM category_table
WHERE active = true AND company_code = $1
)
AND company_code = $1
`;
```
### 실수 2: COUNT/SUM 집계 함수
```typescript
// ❌ 잘못된 방법 - 모든 회사의 총합
const query = `SELECT COUNT(*) as total FROM example_table`;
// ✅ 올바른 방법
const query = `
SELECT COUNT(*) as total
FROM example_table
WHERE company_code = $1
`;
```
### 실수 3: autoFilter 누락
```typescript
// ❌ 잘못된 방법
const response = await apiClient.post(
`/table-management/tables/${tableName}/data`,
{
page,
size,
search,
}
);
// ✅ 올바른 방법
const response = await apiClient.post(
`/table-management/tables/${tableName}/data`,
{
page,
size,
search,
autoFilter: {
enabled: true,
filterColumn: "company_code",
userField: "companyCode",
},
}
);
```
---
## 12. 참고 자료
### 완료된 구현 예시
- **테이블 데이터 API**: `backend-node/src/controllers/tableManagementController.ts` (getTableData)
- **Entity 조인 API**: `backend-node/src/controllers/entityJoinController.ts` (getTableDataWithJoins)
- **카테고리 값 API**: `backend-node/src/services/tableCategoryValueService.ts` (getCategoryValues)
- **프론트엔드 API**: `frontend/lib/api/screen.ts` (getTableData)
- **프론트엔드 Entity 조인**: `frontend/lib/api/entityJoin.ts` (getTableDataWithJoins)
### 마이그레이션 스크립트
- `db/migrations/044_simple_version.sql` - table_type_columns에 company_code 추가
- `db/migrations/045_add_company_code_to_category_values.sql` - 카테고리 값 테이블 마이그레이션
---
## 요약: 절대 잊지 말아야 할 핵심 규칙
### 데이터베이스
1. **모든 테이블에 `company_code` 필수** (`company_mng` 제외)
2. **인덱스와 외래키 필수**
3. **복합 유니크 제약조건에 `company_code` 포함**
### 백엔드 API
1. **모든 SELECT 쿼리**: `WHERE company_code = $1` (최고 관리자 제외)
2. **모든 INSERT 쿼리**: `company_code` 컬럼 포함
3. **모든 UPDATE/DELETE 쿼리**: WHERE 절에 `company_code` 조건 포함
4. **JOIN/서브쿼리/집계**: 모두 `company_code` 필터링 필수
### 프론트엔드
1. **모든 테이블 데이터 API 호출**: `autoFilter` 전달 필수
2. **`company_code`를 직접 전달 금지**: 백엔드에서 자동 처리
---
**🚨 멀티테넌시는 보안의 핵심입니다. 예외 없이 모든 규칙을 준수하세요!**
**⚠️ company_code = "\*"는 공통 데이터가 아닌 최고 관리자 전용 데이터입니다!**
**✅ 모든 테이블에 company_code 필수! (company_mng 제외)**
---
## 🤖 AI 에이전트 필수 체크리스트
**모든 코드 작성/수정 완료 후 반드시 다음을 확인하세요:**
### 데이터베이스 마이그레이션을 작성했다면:
- [ ] `company_code VARCHAR(20) NOT NULL` 컬럼 추가했는가?
- [ ] `company_code`에 인덱스를 생성했는가?
- [ ] `company_mng` 테이블에 대한 외래키를 추가했는가?
- [ ] 복합 유니크 제약조건에 `company_code`를 포함했는가?
- [ ] 기존 데이터를 모든 회사별로 복제했는가?
### 백엔드 API를 작성/수정했다면:
- [ ] SELECT 쿼리에 `WHERE company_code = $1` 조건이 있는가? (최고 관리자 제외)
- [ ] INSERT 쿼리에 `company_code` 컬럼이 포함되어 있는가?
- [ ] UPDATE/DELETE 쿼리의 WHERE 절에 `company_code` 조건이 있는가?
- [ ] JOIN 쿼리의 ON 절에 `company_code` 매칭 조건이 있는가?
- [ ] 서브쿼리에 `company_code` 필터링이 있는가?
- [ ] 집계 함수(COUNT, SUM 등)에 `company_code` 필터링이 있는가?
- [ ] `req.user.companyCode`를 사용하고 있는가? (클라이언트 입력 사용 금지)
- [ ] 최고 관리자(`company_code = "*"`) 예외 처리를 했는가?
- [ ] 로그에 `companyCode` 정보를 포함했는가?
- [ ] 권한 없음 시 적절한 HTTP 상태 코드(404/403)를 반환하는가?
### 프론트엔드 API 호출을 작성/수정했다면:
- [ ] `autoFilter` 옵션을 전달하고 있는가?
- [ ] `autoFilter.enabled = true`로 설정했는가?
- [ ] `autoFilter.filterColumn = "company_code"`로 설정했는가?
- [ ] `autoFilter.userField = "companyCode"`로 설정했는가?
- [ ] `company_code`를 직접 전달하지 않았는가? (백엔드 자동 처리)
### 테스트를 수행했다면:
- [ ] 회사 A로 로그인하여 회사 A 데이터만 보이는지 확인했는가?
- [ ] 회사 B로 로그인하여 회사 B 데이터만 보이는지 확인했는가?
- [ ] 회사 A가 회사 B 데이터에 접근할 수 없는지 확인했는가?
- [ ] 최고 관리자로 로그인하여 모든 데이터가 보이는지 확인했는가?
---
**⚠️ 위 체크리스트 중 하나라도 "아니오"가 있다면, 코드를 다시 검토하세요!**
**🚨 멀티테넌시 위반은 치명적인 보안 취약점입니다!**

View File

@ -0,0 +1,559 @@
# 다국어 지원 컴포넌트 개발 가이드
새로운 화면 컴포넌트를 개발할 때 반드시 다국어 시스템을 고려해야 합니다.
이 가이드는 컴포넌트가 다국어 자동 생성 및 매핑 시스템과 호환되도록 하는 방법을 설명합니다.
---
## 1. 타입 정의 시 다국어 필드 추가
### 기본 원칙
텍스트가 표시되는 **모든 속성**에 `langKeyId`와 `langKey` 필드를 함께 정의해야 합니다.
### 단일 텍스트 속성
```typescript
interface MyComponentConfig {
// 기본 텍스트
title?: string;
// 다국어 키 (필수 추가)
titleLangKeyId?: number;
titleLangKey?: string;
// 라벨
label?: string;
labelLangKeyId?: number;
labelLangKey?: string;
// 플레이스홀더
placeholder?: string;
placeholderLangKeyId?: number;
placeholderLangKey?: string;
}
```
### 배열/목록 속성 (컬럼, 탭 등)
```typescript
interface ColumnConfig {
name: string;
label: string;
// 다국어 키 (필수 추가)
langKeyId?: number;
langKey?: string;
// 기타 속성
width?: number;
align?: "left" | "center" | "right";
}
interface TabConfig {
id: string;
label: string;
// 다국어 키 (필수 추가)
langKeyId?: number;
langKey?: string;
// 탭 제목도 별도로
title?: string;
titleLangKeyId?: number;
titleLangKey?: string;
}
interface MyComponentConfig {
columns?: ColumnConfig[];
tabs?: TabConfig[];
}
```
### 버튼 컴포넌트
```typescript
interface ButtonComponentConfig {
text?: string;
// 다국어 키 (필수 추가)
langKeyId?: number;
langKey?: string;
}
```
### 실제 예시: 분할 패널
```typescript
interface SplitPanelLayoutConfig {
leftPanel?: {
title?: string;
langKeyId?: number; // 좌측 패널 제목 다국어
langKey?: string;
columns?: Array<{
name: string;
label: string;
langKeyId?: number; // 각 컬럼 다국어
langKey?: string;
}>;
};
rightPanel?: {
title?: string;
langKeyId?: number; // 우측 패널 제목 다국어
langKey?: string;
columns?: Array<{
name: string;
label: string;
langKeyId?: number;
langKey?: string;
}>;
additionalTabs?: Array<{
label: string;
langKeyId?: number; // 탭 라벨 다국어
langKey?: string;
title?: string;
titleLangKeyId?: number; // 탭 제목 다국어
titleLangKey?: string;
columns?: Array<{
name: string;
label: string;
langKeyId?: number;
langKey?: string;
}>;
}>;
};
}
```
---
## 2. 라벨 추출 로직 등록
### 파일 위치
`frontend/lib/utils/multilangLabelExtractor.ts`
### `extractMultilangLabels` 함수에 추가
새 컴포넌트의 라벨을 추출하는 로직을 추가해야 합니다.
```typescript
// 새 컴포넌트 타입 체크
if (comp.componentType === "my-new-component") {
const config = comp.componentConfig as MyComponentConfig;
// 1. 제목 추출
if (config?.title) {
addLabel({
id: `${comp.id}_title`,
componentId: `${comp.id}_title`,-
label: config.title,
type: "title",
parentType: "my-new-component",
parentLabel: config.title,
langKeyId: config.titleLangKeyId,
langKey: config.titleLangKey,
});
}
// 2. 컬럼 추출
if (config?.columns && Array.isArray(config.columns)) {
config.columns.forEach((col, index) => {
const colLabel = col.label || col.name;
addLabel({
id: `${comp.id}_col_${index}`,
componentId: `${comp.id}_col_${index}`,
label: colLabel,
type: "column",
parentType: "my-new-component",
parentLabel: config.title || "새 컴포넌트",
langKeyId: col.langKeyId,
langKey: col.langKey,
});
});
}
// 3. 버튼 텍스트 추출 (버튼 컴포넌트인 경우)
if (config?.text) {
addLabel({
id: `${comp.id}_button`,
componentId: `${comp.id}_button`,
label: config.text,
type: "button",
parentType: "my-new-component",
parentLabel: config.text,
langKeyId: config.langKeyId,
langKey: config.langKey,
});
}
}
```
### 추출해야 할 라벨 타입
| 타입 | 설명 | 예시 |
| ------------- | ------------------ | ------------------------ |
| `title` | 컴포넌트/패널 제목 | 분할패널 제목, 카드 제목 |
| `label` | 입력 필드 라벨 | 텍스트 입력 라벨 |
| `button` | 버튼 텍스트 | 저장, 취소, 삭제 |
| `column` | 테이블 컬럼 헤더 | 품목명, 수량, 금액 |
| `tab` | 탭 라벨 | 기본정보, 상세정보 |
| `filter` | 검색 필터 라벨 | 검색어, 기간 |
| `placeholder` | 플레이스홀더 | "검색어를 입력하세요" |
| `action` | 액션 버튼/링크 | 수정, 삭제, 상세보기 |
---
## 3. 매핑 적용 로직 등록
### 파일 위치
`frontend/lib/utils/multilangLabelExtractor.ts`
### `applyMultilangMappings` 함수에 추가
다국어 키가 선택되면 컴포넌트에 `langKeyId`와 `langKey`를 저장하는 로직을 추가합니다.
```typescript
// 새 컴포넌트 매핑 적용
if (comp.componentType === "my-new-component") {
const config = comp.componentConfig as MyComponentConfig;
// 1. 제목 매핑
const titleMapping = mappingMap.get(`${comp.id}_title`);
if (titleMapping) {
updated.componentConfig = {
...updated.componentConfig,
titleLangKeyId: titleMapping.keyId,
titleLangKey: titleMapping.langKey,
};
}
// 2. 컬럼 매핑
if (config?.columns && Array.isArray(config.columns)) {
const updatedColumns = config.columns.map((col, index) => {
const colMapping = mappingMap.get(`${comp.id}_col_${index}`);
if (colMapping) {
return {
...col,
langKeyId: colMapping.keyId,
langKey: colMapping.langKey,
};
}
return col;
});
updated.componentConfig = {
...updated.componentConfig,
columns: updatedColumns,
};
}
// 3. 버튼 매핑 (버튼 컴포넌트인 경우)
const buttonMapping = mappingMap.get(`${comp.id}_button`);
if (buttonMapping) {
updated.componentConfig = {
...updated.componentConfig,
langKeyId: buttonMapping.keyId,
langKey: buttonMapping.langKey,
};
}
}
```
### 주의사항
- **객체 참조 유지**: 매핑 시 기존 `updated.componentConfig`를 기반으로 업데이트해야 합니다.
- **중첩 구조**: 중첩된 객체(예: `leftPanel.columns`)는 상위 객체부터 순서대로 업데이트합니다.
```typescript
// 잘못된 방법 - 이전 업데이트 덮어쓰기
updated.componentConfig = { ...config, langKeyId: mapping.keyId }; // ❌
updated.componentConfig = { ...config, columns: updatedColumns }; // langKeyId 사라짐!
// 올바른 방법 - 이전 업데이트 유지
updated.componentConfig = {
...updated.componentConfig,
langKeyId: mapping.keyId,
}; // ✅
updated.componentConfig = {
...updated.componentConfig,
columns: updatedColumns,
}; // ✅
```
---
## 4. 번역 표시 로직 구현
### 파일 위치
새 컴포넌트 파일 (예: `frontend/lib/registry/components/my-component/MyComponent.tsx`)
### Context 사용
```typescript
import { useScreenMultiLang } from "@/contexts/ScreenMultiLangContext";
const MyComponent = ({ component }: Props) => {
const { getTranslatedText } = useScreenMultiLang();
const config = component.componentConfig;
// 제목 번역
const displayTitle = config?.titleLangKey
? getTranslatedText(config.titleLangKey, config.title || "")
: config?.title || "";
// 컬럼 헤더 번역
const translatedColumns = config?.columns?.map((col) => ({
...col,
displayLabel: col.langKey
? getTranslatedText(col.langKey, col.label)
: col.label,
}));
// 버튼 텍스트 번역
const buttonText = config?.langKey
? getTranslatedText(config.langKey, config.text || "")
: config?.text || "";
return (
<div>
<h2>{displayTitle}</h2>
<table>
<thead>
<tr>
{translatedColumns?.map((col, idx) => (
<th key={idx}>{col.displayLabel}</th>
))}
</tr>
</thead>
</table>
<button>{buttonText}</button>
</div>
);
};
```
### getTranslatedText 함수
```typescript
// 첫 번째 인자: langKey (다국어 키)
// 두 번째 인자: fallback (키가 없거나 번역이 없을 때 기본값)
const text = getTranslatedText(
"screen.company_1.Sales.OrderList.품목명",
"품목명"
);
```
### 주의사항
- `langKey`가 없으면 원본 텍스트를 표시합니다.
- `useScreenMultiLang`은 반드시 `ScreenMultiLangProvider` 내부에서 사용해야 합니다.
- 화면 페이지(`/screens/[screenId]/page.tsx`)에서 이미 Provider로 감싸져 있습니다.
---
## 5. ScreenMultiLangContext에 키 수집 로직 추가
### 파일 위치
`frontend/contexts/ScreenMultiLangContext.tsx`
### `collectLangKeys` 함수에 추가
번역을 미리 로드하기 위해 컴포넌트에서 사용하는 모든 `langKey`를 수집해야 합니다.
```typescript
const collectLangKeys = (comps: ComponentData[]): Set<string> => {
const keys = new Set<string>();
const processComponent = (comp: ComponentData) => {
const config = comp.componentConfig;
// 새 컴포넌트의 langKey 수집
if (comp.componentType === "my-new-component") {
// 제목
if (config?.titleLangKey) {
keys.add(config.titleLangKey);
}
// 컬럼
if (config?.columns && Array.isArray(config.columns)) {
config.columns.forEach((col: any) => {
if (col.langKey) {
keys.add(col.langKey);
}
});
}
// 버튼
if (config?.langKey) {
keys.add(config.langKey);
}
}
// 자식 컴포넌트 재귀 처리
if (comp.children && Array.isArray(comp.children)) {
comp.children.forEach(processComponent);
}
};
comps.forEach(processComponent);
return keys;
};
```
---
## 6. MultilangSettingsModal에 표시 로직 추가
### 파일 위치
`frontend/components/screen/modals/MultilangSettingsModal.tsx`
### `extractLabelsFromComponents` 함수에 추가
다국어 설정 모달에서 새 컴포넌트의 라벨이 표시되도록 합니다.
```typescript
// 새 컴포넌트 라벨 추출
if (comp.componentType === "my-new-component") {
const config = comp.componentConfig as MyComponentConfig;
// 제목
if (config?.title) {
addLabel({
id: `${comp.id}_title`,
componentId: `${comp.id}_title`,
label: config.title,
type: "title",
parentType: "my-new-component",
parentLabel: config.title,
langKeyId: config.titleLangKeyId,
langKey: config.titleLangKey,
});
}
// 컬럼
if (config?.columns) {
config.columns.forEach((col, index) => {
// columnLabelMap에서 라벨 가져오기 (테이블 컬럼인 경우)
const tableName = config.tableName;
const displayLabel =
tableName && columnLabelMap[tableName]?.[col.name]
? columnLabelMap[tableName][col.name]
: col.label || col.name;
addLabel({
id: `${comp.id}_col_${index}`,
componentId: `${comp.id}_col_${index}`,
label: displayLabel,
type: "column",
parentType: "my-new-component",
parentLabel: config.title || "새 컴포넌트",
langKeyId: col.langKeyId,
langKey: col.langKey,
});
});
}
}
```
---
## 7. 테이블명 추출 (테이블 사용 컴포넌트인 경우)
### 파일 위치
`frontend/lib/utils/multilangLabelExtractor.ts`
### `extractTableNames` 함수에 추가
컴포넌트가 테이블을 사용하는 경우, 테이블명을 추출해야 컬럼 라벨을 가져올 수 있습니다.
```typescript
const extractTableNames = (comps: ComponentData[]): Set<string> => {
const tableNames = new Set<string>();
const processComponent = (comp: ComponentData) => {
const config = comp.componentConfig;
// 새 컴포넌트의 테이블명 추출
if (comp.componentType === "my-new-component") {
if (config?.tableName) {
tableNames.add(config.tableName);
}
if (config?.selectedTable) {
tableNames.add(config.selectedTable);
}
}
// 자식 컴포넌트 재귀 처리
if (comp.children && Array.isArray(comp.children)) {
comp.children.forEach(processComponent);
}
};
comps.forEach(processComponent);
return tableNames;
};
```
---
## 8. 체크리스트
새 컴포넌트 개발 시 다음 항목을 확인하세요:
### 타입 정의
- [ ] 모든 텍스트 속성에 `langKeyId`, `langKey` 필드 추가
- [ ] 배열 속성(columns, tabs 등)의 각 항목에도 다국어 필드 추가
### 라벨 추출 (multilangLabelExtractor.ts)
- [ ] `extractMultilangLabels` 함수에 라벨 추출 로직 추가
- [ ] `extractTableNames` 함수에 테이블명 추출 로직 추가 (해당되는 경우)
### 매핑 적용 (multilangLabelExtractor.ts)
- [ ] `applyMultilangMappings` 함수에 매핑 적용 로직 추가
### 번역 표시 (컴포넌트 파일)
- [ ] `useScreenMultiLang` 훅 사용
- [ ] `getTranslatedText`로 텍스트 번역 적용
### 키 수집 (ScreenMultiLangContext.tsx)
- [ ] `collectLangKeys` 함수에 langKey 수집 로직 추가
### 설정 모달 (MultilangSettingsModal.tsx)
- [ ] `extractLabelsFromComponents`에 라벨 표시 로직 추가
---
## 9. 관련 파일 목록
| 파일 | 역할 |
| -------------------------------------------------------------- | ----------------------- |
| `frontend/lib/utils/multilangLabelExtractor.ts` | 라벨 추출 및 매핑 적용 |
| `frontend/contexts/ScreenMultiLangContext.tsx` | 번역 Context 및 키 수집 |
| `frontend/components/screen/modals/MultilangSettingsModal.tsx` | 다국어 설정 UI |
| `frontend/components/screen/ScreenDesigner.tsx` | 다국어 생성 버튼 처리 |
| `backend-node/src/services/multilangService.ts` | 다국어 키 생성 서비스 |
---
## 10. 주의사항
1. **componentId 형식 일관성**: 라벨 추출과 매핑 적용에서 동일한 ID 형식 사용
- 제목: `${comp.id}_title`
- 컬럼: `${comp.id}_col_${index}`
- 버튼: `${comp.id}_button`
2. **중첩 구조 주의**: 분할패널처럼 중첩된 구조는 경로를 명확히 지정
- `${comp.id}_left_title`, `${comp.id}_right_col_${index}`
3. **기존 값 보존**: 매핑 적용 시 `updated.componentConfig`를 기반으로 업데이트
4. **라벨 타입 구분**: 입력 폼의 `label`과 다른 컴포넌트의 `label`을 구분하여 처리
5. **테스트**: 다국어 생성 → 다국어 설정 → 언어 변경 순서로 테스트

View File

@ -0,0 +1,310 @@
# TableListComponent 개발 가이드
## 개요
`TableListComponent`는 ERP 시스템의 핵심 데이터 그리드 컴포넌트입니다. DevExpress DataGrid 스타일의 고급 기능들을 구현하고 있습니다.
**파일 위치**: `frontend/lib/registry/components/table-list/TableListComponent.tsx`
---
## 핵심 기능 목록
### 1. 인라인 편집 (Inline Editing)
- 셀 더블클릭 또는 F2 키로 편집 모드 진입
- 직접 타이핑으로도 편집 모드 진입 가능
- Enter로 저장, Escape로 취소
- **컬럼별 편집 가능 여부 설정** (`editable` 속성)
```typescript
// ColumnConfig에서 editable 속성 사용
interface ColumnConfig {
editable?: boolean; // false면 해당 컬럼 인라인 편집 불가
}
```
**편집 불가 컬럼 체크 필수 위치**:
1. `handleCellDoubleClick` - 더블클릭 편집
2. `onKeyDown` F2 케이스 - 키보드 편집
3. `onKeyDown` default 케이스 - 직접 타이핑 편집
4. 컨텍스트 메뉴 "셀 편집" 옵션
### 2. 배치 편집 (Batch Editing)
- 여러 셀 수정 후 일괄 저장/취소
- `pendingChanges` Map으로 변경사항 추적
- 저장 전 유효성 검증
### 3. 데이터 유효성 검증 (Validation)
```typescript
type ValidationRule = {
required?: boolean;
min?: number;
max?: number;
minLength?: number;
maxLength?: number;
pattern?: RegExp;
customMessage?: string;
validate?: (value: any, row: any) => string | null;
};
```
### 4. 컬럼 헤더 필터 (Header Filter)
- 각 컬럼 헤더에 필터 아이콘
- 고유값 목록에서 다중 선택 필터링
- `headerFilters` Map으로 필터 상태 관리
### 5. 필터 빌더 (Filter Builder)
```typescript
interface FilterCondition {
id: string;
column: string;
operator: "equals" | "notEquals" | "contains" | "notContains" |
"startsWith" | "endsWith" | "greaterThan" | "lessThan" |
"greaterOrEqual" | "lessOrEqual" | "isEmpty" | "isNotEmpty";
value: string;
}
interface FilterGroup {
id: string;
logic: "AND" | "OR";
conditions: FilterCondition[];
}
```
### 6. 검색 패널 (Search Panel)
- 전체 데이터 검색
- 검색어 하이라이팅
- `searchHighlights` Map으로 하이라이트 위치 관리
### 7. 엑셀 내보내기 (Excel Export)
- `xlsx` 라이브러리 사용
- 현재 표시 데이터 또는 전체 데이터 내보내기
```typescript
import * as XLSX from "xlsx";
// 사용 예시
const worksheet = XLSX.utils.json_to_sheet(exportData);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1");
XLSX.writeFile(workbook, `${tableName}_${timestamp}.xlsx`);
```
### 8. 클립보드 복사 (Copy to Clipboard)
- 선택된 행 또는 전체 데이터 복사
- 탭 구분자로 엑셀 붙여넣기 호환
### 9. 컨텍스트 메뉴 (Context Menu)
- 우클릭으로 메뉴 표시
- 셀 편집, 행 복사, 행 삭제 등 옵션
- 편집 불가 컬럼은 "(잠김)" 표시
### 10. 키보드 네비게이션
| 키 | 동작 |
|---|---|
| Arrow Keys | 셀 이동 |
| Tab | 다음 셀 |
| Shift+Tab | 이전 셀 |
| F2 | 편집 모드 |
| Enter | 저장 후 아래로 이동 |
| Escape | 편집 취소 |
| Ctrl+C | 복사 |
| Delete | 셀 값 삭제 |
### 11. 컬럼 리사이징
- 컬럼 헤더 경계 드래그로 너비 조절
- `columnWidths` 상태로 관리
- localStorage에 저장
### 12. 컬럼 순서 변경
- 드래그 앤 드롭으로 컬럼 순서 변경
- `columnOrder` 상태로 관리
- localStorage에 저장
### 13. 상태 영속성 (State Persistence)
```typescript
// localStorage 키 패턴
const stateKey = `tableState_${tableName}_${userId}`;
// 저장되는 상태
interface TableState {
columnWidths: Record<string, number>;
columnOrder: string[];
sortBy: string;
sortOrder: "asc" | "desc";
frozenColumns: string[];
columnVisibility: Record<string, boolean>;
}
```
### 14. 그룹화 및 그룹 소계
```typescript
interface GroupedData {
groupKey: string;
groupValues: Record<string, any>;
items: any[];
count: number;
summary?: Record<string, { sum: number; avg: number; count: number }>;
}
```
### 15. 총계 요약 (Total Summary)
- 숫자 컬럼의 합계, 평균, 개수 표시
- 테이블 하단에 요약 행 렌더링
---
## 캐싱 전략
```typescript
// 테이블 컬럼 캐시
const tableColumnCache = new Map<string, { columns: any[]; timestamp: number }>();
const TABLE_CACHE_TTL = 5 * 60 * 1000; // 5분
// API 호출 디바운싱
const debouncedApiCall = <T extends any[], R>(
key: string,
fn: (...args: T) => Promise<R>,
delay: number = 300
) => { ... };
```
---
## 필수 Import
```typescript
import React, { useState, useEffect, useMemo, useCallback, useRef } from "react";
import { TableListConfig, ColumnConfig } from "./types";
import { tableTypeApi } from "@/lib/api/screen";
import { entityJoinApi } from "@/lib/api/entityJoin";
import { codeCache } from "@/lib/caching/codeCache";
import * as XLSX from "xlsx";
import { toast } from "sonner";
```
---
## 주요 상태 (State)
```typescript
// 데이터 관련
const [tableData, setTableData] = useState<any[]>([]);
const [filteredData, setFilteredData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
// 편집 관련
const [editingCell, setEditingCell] = useState<{
rowIndex: number;
colIndex: number;
columnName: string;
originalValue: any;
} | null>(null);
const [editingValue, setEditingValue] = useState<string>("");
const [pendingChanges, setPendingChanges] = useState<Map<string, Map<string, any>>>(new Map());
const [validationErrors, setValidationErrors] = useState<Map<string, Map<string, string>>>(new Map());
// 필터 관련
const [headerFilters, setHeaderFilters] = useState<Map<string, Set<string>>>(new Map());
const [filterGroups, setFilterGroups] = useState<FilterGroup[]>([]);
const [globalSearchText, setGlobalSearchText] = useState("");
const [searchHighlights, setSearchHighlights] = useState<Map<string, number[]>>(new Map());
// 컬럼 관련
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
const [columnOrder, setColumnOrder] = useState<string[]>([]);
const [columnVisibility, setColumnVisibility] = useState<Record<string, boolean>>({});
const [frozenColumns, setFrozenColumns] = useState<string[]>([]);
// 선택 관련
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
const [focusedCell, setFocusedCell] = useState<{ rowIndex: number; colIndex: number } | null>(null);
// 정렬 관련
const [sortBy, setSortBy] = useState<string>("");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
// 페이지네이션
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [totalCount, setTotalCount] = useState(0);
```
---
## 편집 불가 컬럼 구현 체크리스트
새로운 편집 진입점을 추가할 때 반드시 다음을 확인하세요:
- [ ] `column.editable === false` 체크 추가
- [ ] 편집 불가 시 `toast.warning()` 메시지 표시
- [ ] `return` 또는 `break`로 편집 모드 진입 방지
```typescript
// 표준 편집 불가 체크 패턴
const column = visibleColumns.find((col) => col.columnName === columnName);
if (column?.editable === false) {
toast.warning(`'${column.displayName || columnName}' 컬럼은 편집할 수 없습니다.`);
return;
}
```
---
## 시각적 표시
### 편집 불가 컬럼 표시
```tsx
// 헤더에 잠금 아이콘
{column.editable === false && (
<Lock className="ml-1 h-3 w-3 text-muted-foreground" />
)}
// 셀 배경색
className={cn(
column.editable === false && "bg-gray-50 dark:bg-gray-900/30"
)}
```
---
## 성능 최적화
1. **useMemo 사용**: `visibleColumns`, `filteredData`, `paginatedData` 등 계산 비용이 큰 값
2. **useCallback 사용**: 이벤트 핸들러 함수들
3. **디바운싱**: API 호출, 검색, 필터링
4. **캐싱**: 테이블 컬럼 정보, 코드 데이터
---
## 주의사항
1. **visibleColumns 정의 순서**: `columnOrder`, `columnVisibility` 상태 이후에 정의해야 함
2. **editInputRef 타입 체크**: `select()` 호출 전 `instanceof HTMLInputElement` 확인
3. **localStorage 키**: `tableName`과 `userId`를 조합하여 고유하게 생성
4. **멀티테넌시**: 모든 API 호출에 `company_code` 필터링 적용 (백엔드에서 자동 처리)
---
## 관련 파일
- `frontend/lib/registry/components/table-list/types.ts` - 타입 정의
- `frontend/lib/registry/components/table-list/TableListConfigPanel.tsx` - 설정 패널
- `frontend/components/common/TableOptionsModal.tsx` - 옵션 모달
- `frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx` - 스티키 헤더 테이블

View File

@ -0,0 +1,592 @@
# 테이블 타입 관리 SQL 작성 가이드
테이블 타입 관리에서 테이블 생성 시 적용되는 컬럼, 타입, 메타데이터 등록 로직을 기반으로 한 SQL 작성 가이드입니다.
## 핵심 원칙
1. **모든 비즈니스 컬럼은 `VARCHAR(500)`로 통일**: 날짜 타입 외 모든 컬럼은 `VARCHAR(500)`
2. **날짜/시간 컬럼만 `TIMESTAMP` 사용**: `created_date`, `updated_date` 등
3. **기본 컬럼 5개 자동 포함**: 모든 테이블에 id, created_date, updated_date, writer, company_code 필수
4. **3개 메타데이터 테이블 등록 필수**: `table_labels`, `column_labels`, `table_type_columns`
---
## 1. 테이블 생성 DDL 템플릿
### 기본 구조
```sql
CREATE TABLE "테이블명" (
-- 시스템 기본 컬럼 (자동 포함)
"id" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
"created_date" timestamp DEFAULT now(),
"updated_date" timestamp DEFAULT now(),
"writer" varchar(500) DEFAULT NULL,
"company_code" varchar(500),
-- 사용자 정의 컬럼 (모두 VARCHAR(500))
"컬럼1" varchar(500),
"컬럼2" varchar(500),
"컬럼3" varchar(500)
);
```
### 예시: 고객 테이블 생성
```sql
CREATE TABLE "customer_info" (
"id" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
"created_date" timestamp DEFAULT now(),
"updated_date" timestamp DEFAULT now(),
"writer" varchar(500) DEFAULT NULL,
"company_code" varchar(500),
"customer_name" varchar(500),
"customer_code" varchar(500),
"phone" varchar(500),
"email" varchar(500),
"address" varchar(500),
"status" varchar(500),
"registration_date" varchar(500)
);
```
---
## 2. 메타데이터 테이블 등록
테이블 생성 시 반드시 아래 3개 테이블에 메타데이터를 등록해야 합니다.
### 2.1 table_labels (테이블 메타데이터)
```sql
INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date)
VALUES ('테이블명', '테이블 라벨', '테이블 설명', now(), now())
ON CONFLICT (table_name)
DO UPDATE SET
table_label = EXCLUDED.table_label,
description = EXCLUDED.description,
updated_date = now();
```
### 2.2 table_type_columns (컬럼 타입 정보)
**필수 컬럼**: `table_name`, `column_name`, `company_code`, `input_type`, `display_order`
```sql
-- 기본 컬럼 등록 (display_order: -5 ~ -1)
INSERT INTO table_type_columns (
table_name, column_name, company_code, input_type, detail_settings,
is_nullable, display_order, created_date, updated_date
) VALUES
('테이블명', 'id', '*', 'text', '{}', 'Y', -5, now(), now()),
('테이블명', 'created_date', '*', 'date', '{}', 'Y', -4, now(), now()),
('테이블명', 'updated_date', '*', 'date', '{}', 'Y', -3, now(), now()),
('테이블명', 'writer', '*', 'text', '{}', 'Y', -2, now(), now()),
('테이블명', 'company_code', '*', 'text', '{}', 'Y', -1, now(), now())
ON CONFLICT (table_name, column_name, company_code)
DO UPDATE SET
input_type = EXCLUDED.input_type,
display_order = EXCLUDED.display_order,
updated_date = now();
-- 사용자 정의 컬럼 등록 (display_order: 0부터 시작)
INSERT INTO table_type_columns (
table_name, column_name, company_code, input_type, detail_settings,
is_nullable, display_order, created_date, updated_date
) VALUES
('테이블명', '컬럼1', '*', 'text', '{}', 'Y', 0, now(), now()),
('테이블명', '컬럼2', '*', 'number', '{}', 'Y', 1, now(), now()),
('테이블명', '컬럼3', '*', 'code', '{"codeCategory":"카테고리코드"}', 'Y', 2, now(), now())
ON CONFLICT (table_name, column_name, company_code)
DO UPDATE SET
input_type = EXCLUDED.input_type,
detail_settings = EXCLUDED.detail_settings,
display_order = EXCLUDED.display_order,
updated_date = now();
```
### 2.3 column_labels (레거시 호환용 - 필수)
```sql
-- 기본 컬럼 등록
INSERT INTO column_labels (
table_name, column_name, column_label, input_type, detail_settings,
description, display_order, is_visible, created_date, updated_date
) VALUES
('테이블명', 'id', 'ID', 'text', '{}', '기본키 (자동생성)', -5, true, now(), now()),
('테이블명', 'created_date', '생성일시', 'date', '{}', '레코드 생성일시', -4, true, now(), now()),
('테이블명', 'updated_date', '수정일시', 'date', '{}', '레코드 수정일시', -3, true, now(), now()),
('테이블명', 'writer', '작성자', 'text', '{}', '레코드 작성자', -2, true, now(), now()),
('테이블명', 'company_code', '회사코드', 'text', '{}', '회사 구분 코드', -1, true, now(), now())
ON CONFLICT (table_name, column_name)
DO UPDATE SET
column_label = EXCLUDED.column_label,
input_type = EXCLUDED.input_type,
detail_settings = EXCLUDED.detail_settings,
description = EXCLUDED.description,
display_order = EXCLUDED.display_order,
is_visible = EXCLUDED.is_visible,
updated_date = now();
-- 사용자 정의 컬럼 등록
INSERT INTO column_labels (
table_name, column_name, column_label, input_type, detail_settings,
description, display_order, is_visible, created_date, updated_date
) VALUES
('테이블명', '컬럼1', '컬럼1 라벨', 'text', '{}', '컬럼1 설명', 0, true, now(), now()),
('테이블명', '컬럼2', '컬럼2 라벨', 'number', '{}', '컬럼2 설명', 1, true, now(), now())
ON CONFLICT (table_name, column_name)
DO UPDATE SET
column_label = EXCLUDED.column_label,
input_type = EXCLUDED.input_type,
detail_settings = EXCLUDED.detail_settings,
description = EXCLUDED.description,
display_order = EXCLUDED.display_order,
is_visible = EXCLUDED.is_visible,
updated_date = now();
```
---
## 3. Input Type 정의
### 지원되는 Input Type 목록
| input_type | 설명 | DB 저장 타입 | UI 컴포넌트 |
| ---------- | ------------- | ------------ | -------------------- |
| `text` | 텍스트 입력 | VARCHAR(500) | Input |
| `number` | 숫자 입력 | VARCHAR(500) | Input (type=number) |
| `date` | 날짜/시간 | VARCHAR(500) | DatePicker |
| `code` | 공통코드 선택 | VARCHAR(500) | Select (코드 목록) |
| `entity` | 엔티티 참조 | VARCHAR(500) | Select (테이블 참조) |
| `select` | 선택 목록 | VARCHAR(500) | Select |
| `checkbox` | 체크박스 | VARCHAR(500) | Checkbox |
| `radio` | 라디오 버튼 | VARCHAR(500) | RadioGroup |
| `textarea` | 긴 텍스트 | VARCHAR(500) | Textarea |
| `file` | 파일 업로드 | VARCHAR(500) | FileUpload |
### WebType → InputType 변환 규칙
```
text, textarea, email, tel, url, password → text
number, decimal → number
date, datetime, time → date
select, dropdown → select
checkbox, boolean → checkbox
radio → radio
code → code
entity → entity
file → text
button → text
```
---
## 4. Detail Settings 설정
### 4.1 Code 타입 (공통코드 참조)
```json
{
"codeCategory": "코드_카테고리_ID"
}
```
```sql
INSERT INTO table_type_columns (..., input_type, detail_settings, ...)
VALUES (..., 'code', '{"codeCategory":"STATUS_CODE"}', ...);
```
### 4.2 Entity 타입 (테이블 참조)
```json
{
"referenceTable": "참조_테이블명",
"referenceColumn": "참조_컬럼명(보통 id)",
"displayColumn": "표시할_컬럼명"
}
```
```sql
INSERT INTO table_type_columns (..., input_type, detail_settings, ...)
VALUES (..., 'entity', '{"referenceTable":"user_info","referenceColumn":"id","displayColumn":"user_name"}', ...);
```
### 4.3 Select 타입 (정적 옵션)
```json
{
"options": [
{ "label": "옵션1", "value": "value1" },
{ "label": "옵션2", "value": "value2" }
]
}
```
---
## 5. 전체 예시: 주문 테이블 생성
### Step 1: DDL 실행
```sql
CREATE TABLE "order_info" (
"id" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
"created_date" timestamp DEFAULT now(),
"updated_date" timestamp DEFAULT now(),
"writer" varchar(500) DEFAULT NULL,
"company_code" varchar(500),
"order_no" varchar(500),
"order_date" varchar(500),
"customer_id" varchar(500),
"total_amount" varchar(500),
"status" varchar(500),
"notes" varchar(500)
);
```
### Step 2: table_labels 등록
```sql
INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date)
VALUES ('order_info', '주문 정보', '주문 관리 테이블', now(), now())
ON CONFLICT (table_name)
DO UPDATE SET
table_label = EXCLUDED.table_label,
description = EXCLUDED.description,
updated_date = now();
```
### Step 3: table_type_columns 등록
```sql
-- 기본 컬럼
INSERT INTO table_type_columns (table_name, column_name, company_code, input_type, detail_settings, is_nullable, display_order, created_date, updated_date)
VALUES
('order_info', 'id', '*', 'text', '{}', 'Y', -5, now(), now()),
('order_info', 'created_date', '*', 'date', '{}', 'Y', -4, now(), now()),
('order_info', 'updated_date', '*', 'date', '{}', 'Y', -3, now(), now()),
('order_info', 'writer', '*', 'text', '{}', 'Y', -2, now(), now()),
('order_info', 'company_code', '*', 'text', '{}', 'Y', -1, now(), now())
ON CONFLICT (table_name, column_name, company_code) DO UPDATE SET input_type = EXCLUDED.input_type, display_order = EXCLUDED.display_order, updated_date = now();
-- 사용자 정의 컬럼
INSERT INTO table_type_columns (table_name, column_name, company_code, input_type, detail_settings, is_nullable, display_order, created_date, updated_date)
VALUES
('order_info', 'order_no', '*', 'text', '{}', 'Y', 0, now(), now()),
('order_info', 'order_date', '*', 'date', '{}', 'Y', 1, now(), now()),
('order_info', 'customer_id', '*', 'entity', '{"referenceTable":"customer_info","referenceColumn":"id","displayColumn":"customer_name"}', 'Y', 2, now(), now()),
('order_info', 'total_amount', '*', 'number', '{}', 'Y', 3, now(), now()),
('order_info', 'status', '*', 'code', '{"codeCategory":"ORDER_STATUS"}', 'Y', 4, now(), now()),
('order_info', 'notes', '*', 'textarea', '{}', 'Y', 5, now(), now())
ON CONFLICT (table_name, column_name, company_code) DO UPDATE SET input_type = EXCLUDED.input_type, detail_settings = EXCLUDED.detail_settings, display_order = EXCLUDED.display_order, updated_date = now();
```
### Step 4: column_labels 등록 (레거시 호환)
```sql
-- 기본 컬럼
INSERT INTO column_labels (table_name, column_name, column_label, input_type, detail_settings, description, display_order, is_visible, created_date, updated_date)
VALUES
('order_info', 'id', 'ID', 'text', '{}', '기본키', -5, true, now(), now()),
('order_info', 'created_date', '생성일시', 'date', '{}', '레코드 생성일시', -4, true, now(), now()),
('order_info', 'updated_date', '수정일시', 'date', '{}', '레코드 수정일시', -3, true, now(), now()),
('order_info', 'writer', '작성자', 'text', '{}', '레코드 작성자', -2, true, now(), now()),
('order_info', 'company_code', '회사코드', 'text', '{}', '회사 구분 코드', -1, true, now(), now())
ON CONFLICT (table_name, column_name) DO UPDATE SET column_label = EXCLUDED.column_label, input_type = EXCLUDED.input_type, updated_date = now();
-- 사용자 정의 컬럼
INSERT INTO column_labels (table_name, column_name, column_label, input_type, detail_settings, description, display_order, is_visible, created_date, updated_date)
VALUES
('order_info', 'order_no', '주문번호', 'text', '{}', '주문 식별 번호', 0, true, now(), now()),
('order_info', 'order_date', '주문일자', 'date', '{}', '주문 발생 일자', 1, true, now(), now()),
('order_info', 'customer_id', '고객', 'entity', '{"referenceTable":"customer_info","referenceColumn":"id","displayColumn":"customer_name"}', '주문 고객', 2, true, now(), now()),
('order_info', 'total_amount', '총금액', 'number', '{}', '주문 총 금액', 3, true, now(), now()),
('order_info', 'status', '상태', 'code', '{"codeCategory":"ORDER_STATUS"}', '주문 상태', 4, true, now(), now()),
('order_info', 'notes', '비고', 'textarea', '{}', '추가 메모', 5, true, now(), now())
ON CONFLICT (table_name, column_name) DO UPDATE SET column_label = EXCLUDED.column_label, input_type = EXCLUDED.input_type, detail_settings = EXCLUDED.detail_settings, description = EXCLUDED.description, display_order = EXCLUDED.display_order, updated_date = now();
```
---
## 6. 컬럼 추가 시
### DDL
```sql
ALTER TABLE "테이블명" ADD COLUMN "새컬럼명" varchar(500);
```
### 메타데이터 등록
```sql
-- table_type_columns
INSERT INTO table_type_columns (table_name, column_name, company_code, input_type, detail_settings, is_nullable, display_order, created_date, updated_date)
VALUES ('테이블명', '새컬럼명', '*', 'text', '{}', 'Y', (SELECT COALESCE(MAX(display_order), 0) + 1 FROM table_type_columns WHERE table_name = '테이블명'), now(), now())
ON CONFLICT (table_name, column_name, company_code) DO UPDATE SET input_type = EXCLUDED.input_type, display_order = EXCLUDED.display_order, updated_date = now();
-- column_labels
INSERT INTO column_labels (table_name, column_name, column_label, input_type, detail_settings, description, display_order, is_visible, created_date, updated_date)
VALUES ('테이블명', '새컬럼명', '새컬럼 라벨', 'text', '{}', '새컬럼 설명', (SELECT COALESCE(MAX(display_order), 0) + 1 FROM column_labels WHERE table_name = '테이블명'), true, now(), now())
ON CONFLICT (table_name, column_name) DO UPDATE SET column_label = EXCLUDED.column_label, input_type = EXCLUDED.input_type, updated_date = now();
```
---
## 7. 로그 테이블 생성 (선택사항)
변경 이력 추적이 필요한 테이블에는 로그 테이블을 생성할 수 있습니다.
### 7.1 로그 테이블 DDL 템플릿
```sql
-- 로그 테이블 생성
CREATE TABLE 테이블명_log (
log_id SERIAL PRIMARY KEY,
operation_type VARCHAR(10) NOT NULL, -- INSERT/UPDATE/DELETE
original_id VARCHAR(100), -- 원본 테이블 PK 값
changed_column VARCHAR(100), -- 변경된 컬럼명
old_value TEXT, -- 변경 전 값
new_value TEXT, -- 변경 후 값
changed_by VARCHAR(50), -- 변경자 ID
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 변경 시각
ip_address VARCHAR(50), -- 변경 요청 IP
user_agent TEXT, -- User Agent
full_row_before JSONB, -- 변경 전 전체 행
full_row_after JSONB -- 변경 후 전체 행
);
-- 인덱스 생성
CREATE INDEX idx_테이블명_log_original_id ON 테이블명_log(original_id);
CREATE INDEX idx_테이블명_log_changed_at ON 테이블명_log(changed_at);
CREATE INDEX idx_테이블명_log_operation ON 테이블명_log(operation_type);
-- 코멘트 추가
COMMENT ON TABLE 테이블명_log IS '테이블명 테이블 변경 이력';
```
### 7.2 트리거 함수 DDL 템플릿
```sql
CREATE OR REPLACE FUNCTION 테이블명_log_trigger_func()
RETURNS TRIGGER AS $$
DECLARE
v_column_name TEXT;
v_old_value TEXT;
v_new_value TEXT;
v_user_id VARCHAR(50);
v_ip_address VARCHAR(50);
BEGIN
v_user_id := current_setting('app.user_id', TRUE);
v_ip_address := current_setting('app.ip_address', TRUE);
IF (TG_OP = 'INSERT') THEN
INSERT INTO 테이블명_log (operation_type, original_id, changed_by, ip_address, full_row_after)
VALUES ('INSERT', NEW.id, v_user_id, v_ip_address, row_to_json(NEW)::jsonb);
RETURN NEW;
ELSIF (TG_OP = 'UPDATE') THEN
FOR v_column_name IN
SELECT column_name
FROM information_schema.columns
WHERE table_name = '테이블명'
AND table_schema = 'public'
LOOP
EXECUTE format('SELECT ($1).%I::TEXT, ($2).%I::TEXT', v_column_name, v_column_name)
INTO v_old_value, v_new_value
USING OLD, NEW;
IF v_old_value IS DISTINCT FROM v_new_value THEN
INSERT INTO 테이블명_log (
operation_type, original_id, changed_column, old_value, new_value,
changed_by, ip_address, full_row_before, full_row_after
)
VALUES (
'UPDATE', NEW.id, v_column_name, v_old_value, v_new_value,
v_user_id, v_ip_address, row_to_json(OLD)::jsonb, row_to_json(NEW)::jsonb
);
END IF;
END LOOP;
RETURN NEW;
ELSIF (TG_OP = 'DELETE') THEN
INSERT INTO 테이블명_log (operation_type, original_id, changed_by, ip_address, full_row_before)
VALUES ('DELETE', OLD.id, v_user_id, v_ip_address, row_to_json(OLD)::jsonb);
RETURN OLD;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
```
### 7.3 트리거 DDL 템플릿
```sql
CREATE TRIGGER 테이블명_audit_trigger
AFTER INSERT OR UPDATE OR DELETE ON 테이블명
FOR EACH ROW EXECUTE FUNCTION 테이블명_log_trigger_func();
```
### 7.4 로그 설정 등록
```sql
INSERT INTO table_log_config (
original_table_name, log_table_name, trigger_name,
trigger_function_name, is_active, created_by, created_at
) VALUES (
'테이블명', '테이블명_log', '테이블명_audit_trigger',
'테이블명_log_trigger_func', 'Y', '생성자ID', now()
);
```
### 7.5 table_labels에 use_log_table 플래그 설정
```sql
UPDATE table_labels
SET use_log_table = 'Y', updated_date = now()
WHERE table_name = '테이블명';
```
### 7.6 전체 예시: order_info 로그 테이블 생성
```sql
-- Step 1: 로그 테이블 생성
CREATE TABLE order_info_log (
log_id SERIAL PRIMARY KEY,
operation_type VARCHAR(10) NOT NULL,
original_id VARCHAR(100),
changed_column VARCHAR(100),
old_value TEXT,
new_value TEXT,
changed_by VARCHAR(50),
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ip_address VARCHAR(50),
user_agent TEXT,
full_row_before JSONB,
full_row_after JSONB
);
CREATE INDEX idx_order_info_log_original_id ON order_info_log(original_id);
CREATE INDEX idx_order_info_log_changed_at ON order_info_log(changed_at);
CREATE INDEX idx_order_info_log_operation ON order_info_log(operation_type);
COMMENT ON TABLE order_info_log IS 'order_info 테이블 변경 이력';
-- Step 2: 트리거 함수 생성
CREATE OR REPLACE FUNCTION order_info_log_trigger_func()
RETURNS TRIGGER AS $$
DECLARE
v_column_name TEXT;
v_old_value TEXT;
v_new_value TEXT;
v_user_id VARCHAR(50);
v_ip_address VARCHAR(50);
BEGIN
v_user_id := current_setting('app.user_id', TRUE);
v_ip_address := current_setting('app.ip_address', TRUE);
IF (TG_OP = 'INSERT') THEN
INSERT INTO order_info_log (operation_type, original_id, changed_by, ip_address, full_row_after)
VALUES ('INSERT', NEW.id, v_user_id, v_ip_address, row_to_json(NEW)::jsonb);
RETURN NEW;
ELSIF (TG_OP = 'UPDATE') THEN
FOR v_column_name IN
SELECT column_name FROM information_schema.columns
WHERE table_name = 'order_info' AND table_schema = 'public'
LOOP
EXECUTE format('SELECT ($1).%I::TEXT, ($2).%I::TEXT', v_column_name, v_column_name)
INTO v_old_value, v_new_value USING OLD, NEW;
IF v_old_value IS DISTINCT FROM v_new_value THEN
INSERT INTO order_info_log (operation_type, original_id, changed_column, old_value, new_value, changed_by, ip_address, full_row_before, full_row_after)
VALUES ('UPDATE', NEW.id, v_column_name, v_old_value, v_new_value, v_user_id, v_ip_address, row_to_json(OLD)::jsonb, row_to_json(NEW)::jsonb);
END IF;
END LOOP;
RETURN NEW;
ELSIF (TG_OP = 'DELETE') THEN
INSERT INTO order_info_log (operation_type, original_id, changed_by, ip_address, full_row_before)
VALUES ('DELETE', OLD.id, v_user_id, v_ip_address, row_to_json(OLD)::jsonb);
RETURN OLD;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- Step 3: 트리거 생성
CREATE TRIGGER order_info_audit_trigger
AFTER INSERT OR UPDATE OR DELETE ON order_info
FOR EACH ROW EXECUTE FUNCTION order_info_log_trigger_func();
-- Step 4: 로그 설정 등록
INSERT INTO table_log_config (original_table_name, log_table_name, trigger_name, trigger_function_name, is_active, created_by, created_at)
VALUES ('order_info', 'order_info_log', 'order_info_audit_trigger', 'order_info_log_trigger_func', 'Y', 'system', now());
-- Step 5: table_labels 플래그 업데이트
UPDATE table_labels SET use_log_table = 'Y', updated_date = now() WHERE table_name = 'order_info';
```
### 7.7 로그 테이블 삭제
```sql
-- 트리거 삭제
DROP TRIGGER IF EXISTS 테이블명_audit_trigger ON 테이블명;
-- 트리거 함수 삭제
DROP FUNCTION IF EXISTS 테이블명_log_trigger_func();
-- 로그 테이블 삭제
DROP TABLE IF EXISTS 테이블명_log;
-- 로그 설정 삭제
DELETE FROM table_log_config WHERE original_table_name = '테이블명';
-- table_labels 플래그 업데이트
UPDATE table_labels SET use_log_table = 'N', updated_date = now() WHERE table_name = '테이블명';
```
---
## 8. 체크리스트
### 테이블 생성/수정 시 반드시 확인할 사항:
- [ ] DDL에 기본 5개 컬럼 포함 (id, created_date, updated_date, writer, company_code)
- [ ] 모든 비즈니스 컬럼은 `VARCHAR(500)` 타입 사용
- [ ] `table_labels`에 테이블 메타데이터 등록
- [ ] `table_type_columns`에 모든 컬럼 등록 (company_code = '\*')
- [ ] `column_labels`에 모든 컬럼 등록 (레거시 호환)
- [ ] 기본 컬럼 display_order: -5 ~ -1
- [ ] 사용자 정의 컬럼 display_order: 0부터 순차
- [ ] code/entity 타입은 detail_settings에 참조 정보 포함
- [ ] ON CONFLICT 절로 중복 시 UPDATE 처리
### 로그 테이블 생성 시 확인할 사항 (선택):
- [ ] 로그 테이블 생성 (`테이블명_log`)
- [ ] 인덱스 3개 생성 (original_id, changed_at, operation_type)
- [ ] 트리거 함수 생성 (`테이블명_log_trigger_func`)
- [ ] 트리거 생성 (`테이블명_audit_trigger`)
- [ ] `table_log_config`에 로그 설정 등록
- [ ] `table_labels.use_log_table = 'Y'` 업데이트
---
## 9. 금지 사항
1. **DB 타입 직접 지정 금지**: NUMBER, INTEGER, DATE 등 DB 타입 직접 사용 금지
2. **VARCHAR 길이 변경 금지**: 반드시 `VARCHAR(500)` 사용
3. **기본 컬럼 누락 금지**: id, created_date, updated_date, writer, company_code 필수
4. **메타데이터 미등록 금지**: 3개 테이블 모두 등록 필수
5. **web_type 사용 금지**: 레거시 컬럼이므로 `input_type` 사용
---
## 참조 파일
- `backend-node/src/services/ddlExecutionService.ts`: DDL 실행 서비스
- `backend-node/src/services/tableManagementService.ts`: 로그 테이블 생성 서비스
- `backend-node/src/types/ddl.ts`: DDL 타입 정의
- `backend-node/src/controllers/ddlController.ts`: DDL API 컨트롤러
- `backend-node/src/controllers/tableManagementController.ts`: 로그 테이블 API 컨트롤러

View File

@ -1,5 +1,23 @@
# Cursor Rules for ERP-node Project # Cursor Rules for ERP-node Project
## 🚨 최우선 보안 규칙: 멀티테넌시
**모든 코드 작성/수정 완료 후 반드시 다음 파일을 확인하세요:**
- [멀티테넌시 필수 구현 가이드](.cursor/rules/multi-tenancy-guide.mdc)
**AI 에이전트는 다음 상황에서 반드시 멀티테넌시 체크리스트를 확인해야 합니다:**
1. 데이터베이스 마이그레이션 작성 시
2. 백엔드 API (SELECT/INSERT/UPDATE/DELETE) 작성/수정 시
3. 프론트엔드 데이터 API 호출 작성/수정 시
4. 테스트 완료 시
**핵심 원칙:**
- ✅ 모든 테이블에 `company_code` 필수 (company_mng 제외)
- ✅ 모든 쿼리에 `company_code` 필터링 필수
- ✅ 프론트엔드 API 호출 시 `autoFilter` 전달 필수
---
## shadcn/ui 웹 스타일 가이드라인 ## shadcn/ui 웹 스타일 가이드라인
모든 프론트엔드 개발 시 다음 shadcn/ui 기반 스타일 가이드라인을 준수해야 합니다. 모든 프론트엔드 개발 시 다음 shadcn/ui 기반 스타일 가이드라인을 준수해야 합니다.

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,312 +0,0 @@
# 카드 컴포넌트 기능 확장 계획
## 📋 프로젝트 개요
테이블 리스트 컴포넌트의 고급 기능들(Entity 조인, 필터, 검색, 페이지네이션)을 카드 컴포넌트에도 적용하여 일관된 사용자 경험을 제공합니다.
## 🔍 현재 상태 분석
### ✅ 기존 기능
- 테이블 데이터를 카드 형태로 표시
- 기본적인 컬럼 매핑 (제목, 부제목, 설명, 이미지)
- 카드 레이아웃 설정 (행당 카드 수, 간격)
- 설정 패널 존재
### ❌ 부족한 기능
- Entity 조인 기능
- 필터 및 검색 기능
- 페이지네이션
- 코드 변환 기능
- 정렬 기능
## 🎯 개발 단계
### Phase 1: 타입 및 인터페이스 확장 ⚡
#### 1.1 새로운 타입 정의 추가
```typescript
// CardDisplayConfig 확장
interface CardFilterConfig {
enabled: boolean;
quickSearch: boolean;
showColumnSelector?: boolean;
advancedFilter: boolean;
filterableColumns: string[];
}
interface CardPaginationConfig {
enabled: boolean;
pageSize: number;
showSizeSelector: boolean;
showPageInfo: boolean;
pageSizeOptions: number[];
}
interface CardSortConfig {
enabled: boolean;
defaultSort?: {
column: string;
direction: "asc" | "desc";
};
sortableColumns: string[];
}
```
#### 1.2 CardDisplayConfig 확장
- filter, pagination, sort 설정 추가
- Entity 조인 관련 설정 추가
- 코드 변환 관련 설정 추가
### Phase 2: 핵심 기능 구현 🚀
#### 2.1 Entity 조인 기능
- `useEntityJoinOptimization` 훅 적용
- 조인된 컬럼 데이터 매핑
- 코드 변환 기능 (`optimizedConvertCode`)
- 컬럼 메타정보 관리
#### 2.2 데이터 관리 로직
- 검색/필터/정렬이 적용된 데이터 로딩
- 페이지네이션 처리
- 실시간 검색 기능
- 캐시 최적화
#### 2.3 상태 관리
```typescript
// 새로운 상태 추가
const [searchTerm, setSearchTerm] = useState("");
const [selectedSearchColumn, setSelectedSearchColumn] = useState("");
const [sortColumn, setSortColumn] = useState<string | null>(null);
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const [totalItems, setTotalItems] = useState(0);
```
### Phase 3: UI 컴포넌트 구현 🎨
#### 3.1 헤더 영역
```jsx
<div className="card-header">
<h3>{tableConfig.title || tableLabel}</h3>
<div className="search-controls">
{/* 검색바 */}
<Input placeholder="검색..." />
{/* 검색 컬럼 선택기 */}
<select>...</select>
{/* 새로고침 버튼 */}
<Button></Button>
</div>
</div>
```
#### 3.2 카드 그리드 영역
```jsx
<div
className="card-grid"
style={{
display: "grid",
gridTemplateColumns: `repeat(${cardsPerRow}, 1fr)`,
gap: `${cardSpacing}px`,
}}
>
{displayData.map((item, index) => (
<Card key={index}>{/* 카드 내용 렌더링 */}</Card>
))}
</div>
```
#### 3.3 페이지네이션 영역
```jsx
<div className="card-pagination">
<div>
전체 {totalItems}건 중 {startItem}-{endItem} 표시
</div>
<div>
<select>페이지 크기</select>
<Button>◀◀</Button>
<Button></Button>
<span>
{currentPage} / {totalPages}
</span>
<Button></Button>
<Button>▶▶</Button>
</div>
</div>
```
### Phase 4: 설정 패널 확장 ⚙️
#### 4.1 새 탭 추가
- **필터 탭**: 검색 및 필터 설정
- **페이지네이션 탭**: 페이지 관련 설정
- **정렬 탭**: 정렬 기본값 설정
#### 4.2 설정 옵션
```jsx
// 필터 탭
<TabsContent value="filter">
<Checkbox>필터 기능 사용</Checkbox>
<Checkbox>빠른 검색</Checkbox>
<Checkbox>검색 컬럼 선택기 표시</Checkbox>
<Checkbox>고급 필터</Checkbox>
</TabsContent>
// 페이지네이션 탭
<TabsContent value="pagination">
<Checkbox>페이지네이션 사용</Checkbox>
<Input label="페이지 크기" />
<Checkbox>페이지 크기 선택기 표시</Checkbox>
<Checkbox>페이지 정보 표시</Checkbox>
</TabsContent>
```
## 🛠️ 구현 우선순위
### 🟢 High Priority (1-2주)
1. **Entity 조인 기능**: 테이블 리스트의 로직 재사용
2. **기본 검색 기능**: 검색바 및 실시간 검색
3. **페이지네이션**: 카드 개수 제한 및 페이지 이동
### 🟡 Medium Priority (2-3주)
4. **고급 필터**: 컬럼별 필터 옵션
5. **정렬 기능**: 컬럼별 정렬 및 상태 표시
6. **검색 컬럼 선택기**: 특정 컬럼 검색 기능
### 🔵 Low Priority (3-4주)
7. **카드 뷰 옵션**: 그리드/리스트 전환
8. **카드 크기 조절**: 동적 크기 조정
9. **즐겨찾기 필터**: 자주 사용하는 필터 저장
## 📝 기술적 고려사항
### 재사용 가능한 코드
- `useEntityJoinOptimization`
- 필터 및 검색 로직
- 페이지네이션 컴포넌트
- 코드 캐시 시스템
### 성능 최적화
- 가상화 스크롤 (대량 데이터)
- 이미지 지연 로딩
- 메모리 효율적인 렌더링
- 디바운스된 검색
### 일관성 유지
- 테이블 리스트와 동일한 API
- 동일한 설정 구조
- 일관된 스타일링
- 동일한 이벤트 핸들링
## 🗂️ 파일 구조
```
frontend/lib/registry/components/card-display/
├── CardDisplayComponent.tsx # 메인 컴포넌트 (수정)
├── CardDisplayConfigPanel.tsx # 설정 패널 (수정)
├── types.ts # 타입 정의 (수정)
├── index.ts # 기본 설정 (수정)
├── hooks/
│ └── useCardDataManagement.ts # 데이터 관리 훅 (신규)
├── components/
│ ├── CardHeader.tsx # 헤더 컴포넌트 (신규)
│ ├── CardGrid.tsx # 그리드 컴포넌트 (신규)
│ ├── CardPagination.tsx # 페이지네이션 (신규)
│ └── CardFilter.tsx # 필터 컴포넌트 (신규)
└── utils/
└── cardHelpers.ts # 유틸리티 함수 (신규)
```
## ✅ 완료된 단계
### Phase 1: 타입 및 인터페이스 확장 ✅
- ✅ `CardFilterConfig`, `CardPaginationConfig`, `CardSortConfig` 타입 정의
- ✅ `CardColumnConfig` 인터페이스 추가 (Entity 조인 지원)
- ✅ `CardDisplayConfig` 확장 (새로운 기능들 포함)
- ✅ 기본 설정 업데이트 (filter, pagination, sort 기본값)
### Phase 2: Entity 조인 기능 구현 ✅
- ✅ `useEntityJoinOptimization` 훅 적용
- ✅ 컬럼 메타정보 관리 (`columnMeta` 상태)
- ✅ 코드 변환 기능 (`optimizedConvertCode`)
- ✅ Entity 조인을 고려한 데이터 로딩 로직
### Phase 3: 새로운 UI 구조 구현 ✅
- ✅ 헤더 영역 (제목, 검색바, 컬럼 선택기, 새로고침)
- ✅ 카드 그리드 영역 (반응형 그리드, 로딩/오류 상태)
- ✅ 개별 카드 렌더링 (제목, 부제목, 설명, 추가 필드)
- ✅ 푸터/페이지네이션 영역 (페이지 정보, 크기 선택, 네비게이션)
- ✅ 검색 기능 (디바운스, 컬럼 선택)
- ✅ 코드 값 포맷팅 (`formatCellValue`)
### Phase 4: 설정 패널 확장 ✅
- ✅ **탭 기반 UI 구조** - 5개 탭으로 체계적 분류
- ✅ **일반 탭** - 기본 설정, 카드 레이아웃, 스타일 옵션
- ✅ **매핑 탭** - 컬럼 매핑, 동적 표시 컬럼 관리
- ✅ **필터 탭** - 검색 및 필터 설정 옵션
- ✅ **페이징 탭** - 페이지 관련 설정 및 크기 옵션
- ✅ **정렬 탭** - 정렬 기본값 설정
- ✅ **Shadcn/ui 컴포넌트 적용** - 일관된 UI/UX
## 🎉 프로젝트 완료!
### 📊 최종 달성 결과
**🚀 100% 완료** - 모든 계획된 기능이 성공적으로 구현되었습니다!
#### ✅ 구현된 주요 기능들
1. **완전한 데이터 관리**: 테이블 리스트와 동일한 수준의 데이터 로딩, 검색, 필터링, 페이지네이션
2. **Entity 조인 지원**: 관계형 데이터 조인 및 코드 변환 자동화
3. **고급 검색**: 실시간 검색, 컬럼별 검색, 자동 컬럼 선택
4. **완전한 설정 UI**: 5개 탭으로 분류된 직관적인 설정 패널
5. **반응형 카드 그리드**: 설정 가능한 레이아웃과 스타일
#### 🎯 성능 및 사용성
- **성능 최적화**: 디바운스 검색, 배치 코드 로딩, 캐시 활용
- **사용자 경험**: 로딩 상태, 오류 처리, 직관적인 UI
- **일관성**: 테이블 리스트와 완전히 동일한 API 및 기능
#### 📁 완성된 파일 구조
```
frontend/lib/registry/components/card-display/
├── CardDisplayComponent.tsx ✅ 완전 재구현 (Entity 조인, 검색, 페이징)
├── CardDisplayConfigPanel.tsx ✅ 5개 탭 기반 설정 패널
├── types.ts ✅ 확장된 타입 시스템
└── index.ts ✅ 업데이트된 기본 설정
```
---
**🏆 최종 상태**: **완료** (100%)
**🎯 목표 달성**: 테이블 리스트와 동일한 수준의 강력한 카드 컴포넌트 완성
**⚡ 개발 기간**: 계획 대비 빠른 완료 (예상 3-4주 → 실제 1일)
**✨ 품질**: 테이블 리스트 대비 동등하거나 우수한 기능 수준
### 🔥 주요 성과
이제 사용자들은 **테이블 리스트**와 **카드 디스플레이** 중에서 자유롭게 선택하여 동일한 데이터를 서로 다른 형태로 시각화할 수 있습니다. 두 컴포넌트 모두 완전히 동일한 고급 기능을 제공합니다!

304
DEPLOYMENT_GUIDE_KPSLP.md Normal file
View File

@ -0,0 +1,304 @@
# vexplor 프로젝트 NCP Kubernetes 배포 가이드
## 배포 환경
- **Kubernetes 클러스터**: NCP Kubernetes
- **네임스페이스**: apps
- **GitOps 도구**: Argo CD (https://argocd.kpslp.kr)
- **CI/CD**: Jenkins (Kaniko 빌드)
- **컨테이너 레지스트리**: registry.kpslp.kr
## 전제 조건
### 1. GitLab 레포지토리
- [x] 프로젝트 코드 레포: 이미 생성됨 (현재 레포)
- [ ] Helm Charts 레포: `https://gitlab.kpslp.kr/root/helm-charts` 접근 권한 필요
### 2. 필요한 권한
- [ ] GitLab 계정 및 레포지토리 접근 권한
- [ ] Jenkins 프로젝트 생성 권한 또는 담당자 요청
- [ ] Argo CD 접속 계정
- [ ] Container Registry 푸시 권한
---
## 배포 단계
### Step 1: Helm Charts 레포지토리 설정
김욱동 책임님께 다음 사항을 요청하세요:
```
안녕하세요.
vexplor 프로젝트 배포를 위해 다음 작업이 필요합니다:
1. helm-charts 레포지토리 접근 권한 부여
- 레포지토리: https://gitlab.kpslp.kr/root/helm-charts
- 현재 404 오류로 접근 불가
- 계정: [본인 GitLab 사용자명]
2. values 파일 업로드
- 첨부된 values_vexplor.yaml 파일을
- kpslp/values_vexplor.yaml 경로에 업로드해주시거나
- 업로드 방법을 안내해주세요
3. Jenkins 프로젝트 생성
- 프로젝트명: vexplor
- Git 레포지토리: [현재 프로젝트 GitLab URL]
- Jenkinsfile: 프로젝트 루트에 이미 준비됨
감사합니다.
```
**첨부 파일**: `values_vexplor.yaml` (프로젝트 루트에 생성됨)
---
### Step 2: Jenkins 프로젝트 등록
Jenkins에서 새 파이프라인 프로젝트를 생성합니다:
1. **Jenkins 접속** (URL은 담당자에게 문의)
2. **New Item** 클릭
3. **프로젝트명**: `vexplor`
4. **Pipeline** 선택
5. **Pipeline 설정**:
- Definition: `Pipeline script from SCM`
- SCM: `Git`
- Repository URL: `[현재 프로젝트 GitLab URL]`
- Credentials: `gitlab_userpass_root` (또는 담당자가 안내한 credential)
- Branch: `*/main`
- Script Path: `Jenkinsfile`
---
### Step 3: Argo CD 애플리케이션 등록
1. **Argo CD 접속**: https://argocd.kpslp.kr
2. **New App 생성**:
- **Application Name**: `vexplor`
- **Project**: `default`
- **Sync Policy**: `Automatic` (자동 배포) 또는 `Manual` (수동 배포)
- **Auto-Create Namespace**: ✓ (체크)
3. **Source 설정**:
- **Repository URL**: `https://gitlab.kpslp.kr/root/helm-charts`
- **Revision**: `HEAD` 또는 `main`
- **Path**: `kpslp`
- **Helm Values**: `values_vexplor.yaml`
4. **Destination 설정**:
- **Cluster URL**: `https://kubernetes.default.svc` (기본값)
- **Namespace**: `apps`
5. **Create** 클릭
---
### Step 4: 첫 배포 실행
#### 4-1. Git Push로 Jenkins 빌드 트리거
```bash
git add .
git commit -m "feat: NCP Kubernetes 배포 설정 완료"
git push origin main
```
#### 4-2. Jenkins 빌드 모니터링
1. Jenkins에서 `vexplor` 프로젝트 열기
2. 빌드 시작 확인 (자동 트리거 또는 수동 빌드)
3. 로그 확인:
- **Checkout**: Git 소스 다운로드
- **Build**: Docker 이미지 빌드 (`registry.kpslp.kr/slp/vexplor:xxxxx`)
- **Update Image Tag**: helm-charts 레포의 values 파일 업데이트
#### 4-3. Argo CD 배포 확인
1. Argo CD 대시보드에서 `vexplor` 앱 열기
2. **Sync Status**: `OutOfSync``Synced` 변경 확인
3. **Health Status**: `Progressing``Healthy` 변경 확인
4. Pod 상태 확인 (Running 상태여야 함)
---
## 배포 후 확인사항
### 1. Pod 상태 확인
```bash
kubectl get pods -n apps | grep vexplor
```
**예상 출력**:
```
vexplor-xxxxxxxxxx-xxxxx 1/1 Running 0 2m
```
### 2. 서비스 확인
```bash
kubectl get svc -n apps | grep vexplor
```
### 3. Ingress 확인
```bash
kubectl get ingress -n apps | grep vexplor
```
### 4. 로그 확인
```bash
# 전체 로그
kubectl logs -n apps -l app=vexplor
# 최근 50줄
kubectl logs -n apps -l app=vexplor --tail=50
# 실시간 로그 (스트리밍)
kubectl logs -n apps -l app=vexplor -f
```
### 5. 애플리케이션 접속
- **URL**: `https://vexplor.kpslp.kr` (values 파일에 설정한 도메인)
- **헬스체크**: `https://vexplor.kpslp.kr/api/health`
---
## 트러블슈팅
### 문제 1: Jenkins 빌드 실패
**증상**: Build 단계에서 에러 발생
**확인사항**:
- Docker 이미지 빌드 로그 확인
- `Dockerfile`이 프로젝트 루트에 있는지 확인
- 빌드 컨텍스트에 필요한 파일들이 있는지 확인
**해결**:
```bash
# 로컬에서 Docker 빌드 테스트
docker build -f Dockerfile -t vexplor:test .
```
### 문제 2: helm-charts 레포 푸시 실패
**증상**: Update Image Tag 단계에서 실패
**원인**: `gitlab_userpass_root` credential 문제 또는 권한 부족
**해결**: 김욱동 책임님께 credential 확인 요청
### 문제 3: Argo CD Sync 실패
**증상**: `OutOfSync` 상태에서 변경 없음
**확인사항**:
- values 파일이 올바른 경로에 있는지 (`kpslp/values_vexplor.yaml`)
- Argo CD가 helm-charts 레포를 읽을 수 있는지
**해결**: Argo CD에서 수동 Sync 시도 또는 담당자에게 문의
### 문제 4: Pod가 CrashLoopBackOff 상태
**증상**: Pod가 계속 재시작됨
**확인**:
```bash
kubectl describe pod -n apps [pod-name]
kubectl logs -n apps [pod-name] --previous
```
**일반적인 원인**:
- 환경 변수 누락 (DATABASE_HOST 등)
- 데이터베이스 연결 실패
- 포트 바인딩 문제
**해결**:
1. `values_vexplor.yaml``env` 섹션 확인
2. 데이터베이스 서비스명 확인 (`postgres-service.apps.svc.cluster.local`)
3. Secret 설정 확인 (DB 비밀번호 등)
---
## 업데이트 배포 프로세스
코드 수정 후 배포 절차:
```bash
# 1. 코드 수정
git add .
git commit -m "feat: 새로운 기능 추가"
git push origin main
# 2. Jenkins 자동 빌드 (자동 트리거)
# - Git push 감지
# - Docker 이미지 빌드
# - 새 이미지 태그로 values 파일 업데이트
# 3. Argo CD 자동 배포 (Sync Policy가 Automatic인 경우)
# - helm-charts 레포 변경 감지
# - Kubernetes에 새 이미지 배포
# - Rolling Update 수행
```
**수동 배포**: Argo CD 대시보드에서 `Sync` 버튼 클릭
---
## 체크리스트
배포 전 확인사항:
- [ ] Jenkinsfile 수정 완료 (단일 이미지 빌드)
- [ ] Dockerfile 확인 (멀티스테이지 빌드)
- [ ] values_vexplor.yaml 작성 및 업로드
- [ ] Jenkins 프로젝트 생성
- [ ] Argo CD 애플리케이션 등록
- [ ] 환경 변수 설정 (DATABASE_HOST 등)
- [ ] Secret 생성 (DB 비밀번호 등)
- [ ] Ingress 도메인 설정
- [ ] 헬스체크 엔드포인트 확인 (`/api/health`)
---
## 참고 자료
- **Kaniko**: 컨테이너 내에서 Docker 이미지를 빌드하는 도구
- **GitOps**: Git을 Single Source of Truth로 사용하는 배포 방식
- **Argo CD**: GitOps를 위한 Kubernetes CD 도구
- **Helm**: Kubernetes 패키지 매니저
---
## 담당자 연락처
- **NCP 클러스터 관리**: 김욱동 책임 (엘에스티라유텍)
- **Bastion 서버**: 223.130.135.25:22 (Docker 직접 배포용 아님)
- **Argo CD**: https://argocd.kpslp.kr
- **Kubernetes 네임스페이스**: apps
---
## 추가 설정 (선택사항)
### PostgreSQL 데이터베이스 설정
클러스터 내부에 PostgreSQL이 없다면:
```yaml
# values_vexplor.yaml 에 추가
postgresql:
enabled: true
auth:
username: vexplor
password: changeme123 # Secret으로 관리 권장
database: vexplor
primary:
persistence:
enabled: true
size: 10Gi
```
### Secret 생성 (민감 정보)
```bash
kubectl create secret generic vexplor-secrets \
--from-literal=db-password='your-secure-password' \
--from-literal=jwt-secret='your-jwt-secret' \
-n apps
```
### 모니터링 (Prometheus + Grafana)
담당자에게 메트릭 수집 설정 요청

106
Dockerfile Normal file
View File

@ -0,0 +1,106 @@
# ==========================
# 멀티 스테이지 Dockerfile
# - 백엔드: Node.js + Express + TypeScript
# - 프론트엔드: Next.js (프로덕션 빌드)
# ==========================
# ------------------------------
# Stage 1: 백엔드 빌드
# ------------------------------
FROM node:20.10-alpine AS backend-builder
WORKDIR /app/backend
# 백엔드 의존성 설치
COPY backend-node/package*.json ./
RUN npm ci --only=production && \
npm cache clean --force
# 백엔드 소스 복사 및 빌드
COPY backend-node/tsconfig.json ./
COPY backend-node/src ./src
RUN npm install -D typescript @types/node && \
npm run build && \
npm prune --production
# ------------------------------
# Stage 2: 프론트엔드 빌드
# ------------------------------
FROM node:20.10-alpine AS frontend-builder
WORKDIR /app/frontend
# 프론트엔드 의존성 설치
COPY frontend/package*.json ./
RUN npm ci && \
npm cache clean --force
# 프론트엔드 소스 복사
COPY frontend/ ./
# Next.js 프로덕션 빌드 (린트 비활성화)
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
RUN npm run build:no-lint
# ------------------------------
# Stage 3: 최종 런타임 이미지
# ------------------------------
FROM node:20.10-alpine AS runtime
# 보안 강화: 비특권 사용자 생성
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
# 백엔드 런타임 파일 복사
COPY --from=backend-builder --chown=nodejs:nodejs /app/backend/dist ./backend/dist
COPY --from=backend-builder --chown=nodejs:nodejs /app/backend/node_modules ./backend/node_modules
COPY --from=backend-builder --chown=nodejs:nodejs /app/backend/package.json ./backend/package.json
# 프론트엔드 런타임 파일 복사
COPY --from=frontend-builder --chown=nodejs:nodejs /app/frontend/.next ./frontend/.next
COPY --from=frontend-builder --chown=nodejs:nodejs /app/frontend/node_modules ./frontend/node_modules
COPY --from=frontend-builder --chown=nodejs:nodejs /app/frontend/package.json ./frontend/package.json
COPY --from=frontend-builder --chown=nodejs:nodejs /app/frontend/public ./frontend/public
COPY --from=frontend-builder --chown=nodejs:nodejs /app/frontend/next.config.mjs ./frontend/next.config.mjs
# 업로드 디렉토리 생성 (백엔드용)
RUN mkdir -p /app/backend/uploads && \
chown -R nodejs:nodejs /app/backend/uploads
# 시작 스크립트 생성
RUN echo '#!/bin/sh' > /app/start.sh && \
echo 'set -e' >> /app/start.sh && \
echo '' >> /app/start.sh && \
echo '# 백엔드 시작 (백그라운드)' >> /app/start.sh && \
echo 'cd /app/backend' >> /app/start.sh && \
echo 'echo "Starting backend on port 8080..."' >> /app/start.sh && \
echo 'node dist/app.js &' >> /app/start.sh && \
echo 'BACKEND_PID=$!' >> /app/start.sh && \
echo '' >> /app/start.sh && \
echo '# 프론트엔드 시작 (포그라운드)' >> /app/start.sh && \
echo 'cd /app/frontend' >> /app/start.sh && \
echo 'echo "Starting frontend on port 3000..."' >> /app/start.sh && \
echo 'npm start &' >> /app/start.sh && \
echo 'FRONTEND_PID=$!' >> /app/start.sh && \
echo '' >> /app/start.sh && \
echo '# 프로세스 모니터링' >> /app/start.sh && \
echo 'wait $BACKEND_PID $FRONTEND_PID' >> /app/start.sh && \
chmod +x /app/start.sh && \
chown nodejs:nodejs /app/start.sh
# 비특권 사용자로 전환
USER nodejs
# 포트 노출
EXPOSE 3000 8080
# 헬스체크
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
# 컨테이너 시작
CMD ["/app/start.sh"]

View File

@ -1,399 +0,0 @@
# 외부 커넥션 관리 REST API 지원 구현 완료 보고서
## 📋 구현 개요
`/admin/external-connections` 페이지에 REST API 연결 관리 기능을 성공적으로 추가했습니다.
이제 외부 데이터베이스 연결과 REST API 연결을 탭을 통해 통합 관리할 수 있습니다.
---
## ✅ 구현 완료 사항
### 1. 데이터베이스 구조
**파일**: `/Users/dohyeonsu/Documents/ERP-node/db/create_external_rest_api_connections.sql`
- ✅ `external_rest_api_connections` 테이블 생성
- ✅ 인증 타입 (none, api-key, bearer, basic, oauth2) 지원
- ✅ 헤더 정보 JSONB 저장
- ✅ 테스트 결과 저장 (last_test_date, last_test_result, last_test_message)
- ✅ 샘플 데이터 포함 (기상청 API, JSONPlaceholder)
### 2. 백엔드 구현
#### 타입 정의
**파일**: `backend-node/src/types/externalRestApiTypes.ts`
- ✅ ExternalRestApiConnection 인터페이스
- ✅ ExternalRestApiConnectionFilter 인터페이스
- ✅ RestApiTestRequest 인터페이스
- ✅ RestApiTestResult 인터페이스
- ✅ AuthType 타입 정의
#### 서비스 계층
**파일**: `backend-node/src/services/externalRestApiConnectionService.ts`
- ✅ CRUD 메서드 (getConnections, getConnectionById, createConnection, updateConnection, deleteConnection)
- ✅ 연결 테스트 메서드 (testConnection, testConnectionById)
- ✅ 민감 정보 암호화/복호화 (AES-256-GCM)
- ✅ 유효성 검증
- ✅ 인증 타입별 헤더 구성
#### API 라우트
**파일**: `backend-node/src/routes/externalRestApiConnectionRoutes.ts`
- ✅ GET `/api/external-rest-api-connections` - 목록 조회
- ✅ GET `/api/external-rest-api-connections/:id` - 상세 조회
- ✅ POST `/api/external-rest-api-connections` - 연결 생성
- ✅ PUT `/api/external-rest-api-connections/:id` - 연결 수정
- ✅ DELETE `/api/external-rest-api-connections/:id` - 연결 삭제
- ✅ POST `/api/external-rest-api-connections/test` - 연결 테스트 (데이터 기반)
- ✅ POST `/api/external-rest-api-connections/:id/test` - 연결 테스트 (ID 기반)
#### 라우트 등록
**파일**: `backend-node/src/app.ts`
- ✅ externalRestApiConnectionRoutes import
- ✅ `/api/external-rest-api-connections` 경로 등록
### 3. 프론트엔드 구현
#### API 클라이언트
**파일**: `frontend/lib/api/externalRestApiConnection.ts`
- ✅ ExternalRestApiConnectionAPI 클래스
- ✅ CRUD 메서드
- ✅ 연결 테스트 메서드
- ✅ 지원되는 인증 타입 조회
#### 헤더 관리 컴포넌트
**파일**: `frontend/components/admin/HeadersManager.tsx`
- ✅ 동적 키-값 추가/삭제
- ✅ 테이블 형식 UI
- ✅ 실시간 업데이트
#### 인증 설정 컴포넌트
**파일**: `frontend/components/admin/AuthenticationConfig.tsx`
- ✅ 인증 타입 선택
- ✅ API Key 설정 (header/query 선택)
- ✅ Bearer Token 설정
- ✅ Basic Auth 설정
- ✅ OAuth 2.0 설정
- ✅ 타입별 동적 UI 표시
#### REST API 연결 모달
**파일**: `frontend/components/admin/RestApiConnectionModal.tsx`
- ✅ 기본 정보 입력 (연결명, 설명, URL)
- ✅ 헤더 관리 통합
- ✅ 인증 설정 통합
- ✅ 고급 설정 (타임아웃, 재시도)
- ✅ 연결 테스트 기능
- ✅ 테스트 결과 표시
- ✅ 유효성 검증
#### REST API 연결 목록 컴포넌트
**파일**: `frontend/components/admin/RestApiConnectionList.tsx`
- ✅ 연결 목록 테이블
- ✅ 검색 기능 (연결명, URL)
- ✅ 필터링 (인증 타입, 활성 상태)
- ✅ 연결 테스트 버튼 및 결과 표시
- ✅ 편집/삭제 기능
- ✅ 마지막 테스트 정보 표시
#### 메인 페이지 탭 구조
**파일**: `frontend/app/(main)/admin/external-connections/page.tsx`
- ✅ 탭 UI 추가 (Database / REST API)
- ✅ 데이터베이스 연결 탭 (기존 기능)
- ✅ REST API 연결 탭 (신규 기능)
- ✅ 탭 전환 상태 관리
---
## 🎯 주요 기능
### 1. 탭 전환
- 데이터베이스 연결 관리 ↔ REST API 연결 관리 간 탭으로 전환
- 각 탭은 독립적으로 동작
### 2. REST API 연결 관리
- **연결명**: 고유한 이름으로 연결 식별
- **기본 URL**: API의 베이스 URL
- **헤더 설정**: 키-값 쌍으로 HTTP 헤더 관리
- **인증 설정**: 5가지 인증 타입 지원
- 인증 없음 (none)
- API Key (header 또는 query parameter)
- Bearer Token
- Basic Auth
- OAuth 2.0
### 3. 연결 테스트
- 저장 전 연결 테스트 가능
- 테스트 엔드포인트 지정 가능 (선택)
- 응답 시간, 상태 코드 표시
- 테스트 결과 데이터베이스 저장
### 4. 보안
- 민감 정보 암호화 (API 키, 토큰, 비밀번호)
- AES-256-GCM 알고리즘 사용
- 환경 변수로 암호화 키 관리
---
## 📁 생성된 파일 목록
### 데이터베이스
- `db/create_external_rest_api_connections.sql`
### 백엔드
- `backend-node/src/types/externalRestApiTypes.ts`
- `backend-node/src/services/externalRestApiConnectionService.ts`
- `backend-node/src/routes/externalRestApiConnectionRoutes.ts`
### 프론트엔드
- `frontend/lib/api/externalRestApiConnection.ts`
- `frontend/components/admin/HeadersManager.tsx`
- `frontend/components/admin/AuthenticationConfig.tsx`
- `frontend/components/admin/RestApiConnectionModal.tsx`
- `frontend/components/admin/RestApiConnectionList.tsx`
### 수정된 파일
- `backend-node/src/app.ts` (라우트 등록)
- `frontend/app/(main)/admin/external-connections/page.tsx` (탭 구조)
---
## 🚀 사용 방법
### 1. 데이터베이스 테이블 생성
SQL 스크립트를 실행하세요:
```bash
psql -U postgres -d your_database -f db/create_external_rest_api_connections.sql
```
### 2. 백엔드 재시작
암호화 키 환경 변수 설정 (선택):
```bash
export DB_PASSWORD_SECRET="your-secret-key-32-characters-long"
```
백엔드 재시작:
```bash
cd backend-node
npm run dev
```
### 3. 프론트엔드 접속
브라우저에서 다음 URL로 접속:
```
http://localhost:3000/admin/external-connections
```
### 4. REST API 연결 추가
1. "REST API 연결" 탭 클릭
2. "새 연결 추가" 버튼 클릭
3. 연결 정보 입력:
- 연결명 (필수)
- 기본 URL (필수)
- 헤더 설정
- 인증 설정
4. 연결 테스트 (선택)
5. 저장
---
## 🧪 테스트 시나리오
### 테스트 1: 인증 없는 공개 API
```
연결명: JSONPlaceholder
기본 URL: https://jsonplaceholder.typicode.com
인증 타입: 인증 없음
테스트 엔드포인트: /posts/1
```
### 테스트 2: API Key (Query Parameter)
```
연결명: 기상청 API
기본 URL: https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0
인증 타입: API Key
키 위치: Query Parameter
키 이름: serviceKey
키 값: [your-api-key]
테스트 엔드포인트: /getUltraSrtNcst
```
### 테스트 3: Bearer Token
```
연결명: GitHub API
기본 URL: https://api.github.com
인증 타입: Bearer Token
토큰: ghp_your_token_here
헤더:
- Accept: application/vnd.github.v3+json
- User-Agent: YourApp
테스트 엔드포인트: /user
```
---
## 🔧 고급 설정
### 타임아웃 설정
- 기본값: 30000ms (30초)
- 범위: 1000ms ~ 120000ms
### 재시도 설정
- 재시도 횟수: 0~5회
- 재시도 간격: 100ms ~ 10000ms
### 헤더 관리
- 동적 추가/삭제
- 일반적인 헤더:
- `Content-Type: application/json`
- `Accept: application/json`
- `User-Agent: YourApp/1.0`
---
## 🔒 보안 고려사항
### 암호화
- API 키, 토큰, 비밀번호는 자동 암호화
- AES-256-GCM 알고리즘 사용
- 환경 변수 `DB_PASSWORD_SECRET`로 키 관리
### 권한
- 관리자 권한만 접근 가능
- 회사별 데이터 분리 (`company_code`)
### 테스트 제한
- 동시 테스트 실행 제한
- 타임아웃 강제 적용
---
## 📊 데이터베이스 스키마
```sql
external_rest_api_connections
├── id (SERIAL PRIMARY KEY)
├── connection_name (VARCHAR(100) UNIQUE) -- 연결명
├── description (TEXT) -- 설명
├── base_url (VARCHAR(500)) -- 기본 URL
├── default_headers (JSONB) -- 헤더 (키-값)
├── auth_type (VARCHAR(20)) -- 인증 타입
├── auth_config (JSONB) -- 인증 설정
├── timeout (INTEGER) -- 타임아웃
├── retry_count (INTEGER) -- 재시도 횟수
├── retry_delay (INTEGER) -- 재시도 간격
├── company_code (VARCHAR(20)) -- 회사 코드
├── is_active (CHAR(1)) -- 활성 상태
├── created_date (TIMESTAMP) -- 생성일
├── created_by (VARCHAR(50)) -- 생성자
├── updated_date (TIMESTAMP) -- 수정일
├── updated_by (VARCHAR(50)) -- 수정자
├── last_test_date (TIMESTAMP) -- 마지막 테스트 일시
├── last_test_result (CHAR(1)) -- 마지막 테스트 결과
└── last_test_message (TEXT) -- 마지막 테스트 메시지
```
---
## 🎉 완료 요약
### 구현 완료
- ✅ 데이터베이스 테이블 생성
- ✅ 백엔드 API (CRUD + 테스트)
- ✅ 프론트엔드 UI (탭 + 모달 + 목록)
- ✅ 헤더 관리 기능
- ✅ 5가지 인증 타입 지원
- ✅ 연결 테스트 기능
- ✅ 민감 정보 암호화
### 테스트 완료
- ✅ API 엔드포인트 테스트
- ✅ UI 컴포넌트 통합
- ✅ 탭 전환 기능
- ✅ CRUD 작업
- ✅ 연결 테스트
### 문서 완료
- ✅ 계획서 (PHASE_EXTERNAL_CONNECTION_REST_API_ENHANCEMENT.md)
- ✅ 완료 보고서 (본 문서)
- ✅ SQL 스크립트 (주석 포함)
---
## 🚀 다음 단계 (선택 사항)
### 향후 확장 가능성
1. **엔드포인트 프리셋 관리**
- 자주 사용하는 엔드포인트 저장
- 빠른 호출 지원
2. **요청 템플릿**
- HTTP 메서드별 요청 바디 템플릿
- 변수 치환 기능
3. **응답 매핑**
- API 응답을 내부 데이터 구조로 변환
- 매핑 룰 설정
4. **로그 및 모니터링**
- API 호출 이력 기록
- 응답 시간 모니터링
- 오류율 추적
---
**구현 완료일**: 2025-10-21
**버전**: 1.0
**개발자**: AI Assistant
**상태**: 완료 ✅

56
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,56 @@
pipeline {
agent {
label "kaniko"
}
stages {
stage("Checkout") {
steps {
checkout scm
script {
env.GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
env.GIT_AUTHOR_NAME = sh(script: "git log -1 --pretty=format:'%an'", returnStdout: true)
env.GIT_AUTHOR_EMAIL = sh(script: "git log -1 --pretty=format:'%ae'", returnStdout: true)
env.GIT_COMMIT_MESSAGE = sh (script: 'git log -1 --pretty=%B ${GIT_COMMIT}', returnStdout: true).trim()
env.GIT_PROJECT_NAME = GIT_URL.replaceAll('.git$', '').tokenize('/')[-2]
env.GIT_REPO_NAME = GIT_URL.replaceAll('.git$', '').tokenize('/')[-1]
}
}
}
stage("Build") {
steps {
container("kaniko") {
script {
sh "/kaniko/executor --context . --destination registry.kpslp.kr/${GIT_PROJECT_NAME}/${GIT_REPO_NAME}:${GIT_COMMIT_SHORT}"
}
}
}
}
stage("Update Image Tag") {
steps {
deleteDir()
checkout([
$class: 'GitSCM',
branches: [[name: '*/main']],
extensions: [],
userRemoteConfigs: [[credentialsId: 'gitlab_userpass_root', url: "https://gitlab.kpslp.kr/root/helm-charts"]]
])
script {
def valuesYaml = "kpslp/values_${GIT_REPO_NAME}.yaml"
def values = readYaml file: "${valuesYaml}"
values.image.tag = env.GIT_COMMIT_SHORT
writeYaml file: "${valuesYaml}", data: values, overwrite: true
sh "git config user.name '${GIT_AUTHOR_NAME}'"
sh "git config user.email '${GIT_AUTHOR_EMAIL}'"
withCredentials([usernameColonPassword(credentialsId: 'gitlab_userpass_root', variable: 'USERPASS')]) {
sh '''
git add . && \
git commit -m "${GIT_REPO_NAME}: ${GIT_COMMIT_MESSAGE}" && \
git push https://${USERPASS}@gitlab.kpslp.kr/root/helm-charts HEAD:main || true
'''
}
}
}
}
}
}

View File

@ -1,733 +0,0 @@
# 🔐 Phase 1.5: 인증 및 관리자 서비스 Raw Query 전환 계획
## 📋 개요
Phase 2의 핵심 서비스 전환 전에 **인증 및 관리자 시스템**을 먼저 Raw Query로 전환하여 전체 시스템의 안정적인 기반을 구축합니다.
### 🎯 목표
- AuthService의 5개 Prisma 호출 제거
- AdminService의 3개 Prisma 호출 제거 (이미 Raw Query 사용 중)
- AdminController의 28개 Prisma 호출 제거
- 로그인 → 인증 → API 호출 전체 플로우 검증
### 📊 전환 대상
| 서비스 | Prisma 호출 수 | 복잡도 | 우선순위 |
|--------|----------------|--------|----------|
| AuthService | 5개 | 중간 | 🔴 최우선 |
| AdminService | 3개 | 낮음 (이미 Raw Query) | 🟢 확인만 필요 |
| AdminController | 28개 | 중간 | 🟡 2순위 |
---
## 🔍 AuthService 분석
### Prisma 사용 현황 (5개)
```typescript
// Line 21: loginPwdCheck() - 사용자 비밀번호 조회
const userInfo = await prisma.user_info.findUnique({
where: { user_id: userId },
select: { user_password: true },
});
// Line 82: insertLoginAccessLog() - 로그인 로그 기록
await prisma.$executeRaw`INSERT INTO LOGIN_ACCESS_LOG(...)`;
// Line 126: getUserInfo() - 사용자 정보 조회
const userInfo = await prisma.user_info.findUnique({
where: { user_id: userId },
select: { /* 20개 필드 */ },
});
// Line 157: getUserInfo() - 권한 정보 조회
const authInfo = await prisma.authority_sub_user.findMany({
where: { user_id: userId },
include: { authority_master: { select: { auth_name: true } } },
});
// Line 177: getUserInfo() - 회사 정보 조회
const companyInfo = await prisma.company_mng.findFirst({
where: { company_code: userInfo.company_code || "ILSHIN" },
select: { company_name: true },
});
```
### 핵심 메서드
1. **loginPwdCheck()** - 로그인 비밀번호 검증
- user_info 테이블 조회
- 비밀번호 암호화 비교
- 마스터 패스워드 체크
2. **insertLoginAccessLog()** - 로그인 이력 기록
- LOGIN_ACCESS_LOG 테이블 INSERT
- Raw Query 이미 사용 중 (유지)
3. **getUserInfo()** - 사용자 상세 정보 조회
- user_info 테이블 조회 (20개 필드)
- authority_sub_user + authority_master 조인 (권한)
- company_mng 테이블 조회 (회사명)
- PersonBean 타입 변환
4. **processLogin()** - 로그인 전체 프로세스
- 위 3개 메서드 조합
- JWT 토큰 생성
---
## 🛠️ 전환 계획
### Step 1: loginPwdCheck() 전환
**기존 Prisma 코드:**
```typescript
const userInfo = await prisma.user_info.findUnique({
where: { user_id: userId },
select: { user_password: true },
});
```
**새로운 Raw Query 코드:**
```typescript
import { query } from "../database/db";
const result = await query<{ user_password: string }>(
"SELECT user_password FROM user_info WHERE user_id = $1",
[userId]
);
const userInfo = result.length > 0 ? result[0] : null;
```
### Step 2: getUserInfo() 전환 (사용자 정보)
**기존 Prisma 코드:**
```typescript
const userInfo = await prisma.user_info.findUnique({
where: { user_id: userId },
select: {
sabun: true,
user_id: true,
user_name: true,
// ... 20개 필드
},
});
```
**새로운 Raw Query 코드:**
```typescript
const result = await query<{
sabun: string | null;
user_id: string;
user_name: string;
user_name_eng: string | null;
user_name_cn: string | null;
dept_code: string | null;
dept_name: string | null;
position_code: string | null;
position_name: string | null;
email: string | null;
tel: string | null;
cell_phone: string | null;
user_type: string | null;
user_type_name: string | null;
partner_objid: string | null;
company_code: string | null;
locale: string | null;
photo: Buffer | null;
}>(
`SELECT
sabun, user_id, user_name, user_name_eng, user_name_cn,
dept_code, dept_name, position_code, position_name,
email, tel, cell_phone, user_type, user_type_name,
partner_objid, company_code, locale, photo
FROM user_info
WHERE user_id = $1`,
[userId]
);
const userInfo = result.length > 0 ? result[0] : null;
```
### Step 3: getUserInfo() 전환 (권한 정보)
**기존 Prisma 코드:**
```typescript
const authInfo = await prisma.authority_sub_user.findMany({
where: { user_id: userId },
include: {
authority_master: {
select: { auth_name: true },
},
},
});
const authNames = authInfo
.filter((auth: any) => auth.authority_master?.auth_name)
.map((auth: any) => auth.authority_master!.auth_name!)
.join(",");
```
**새로운 Raw Query 코드:**
```typescript
const authResult = await query<{ auth_name: string }>(
`SELECT am.auth_name
FROM authority_sub_user asu
INNER JOIN authority_master am ON asu.auth_code = am.auth_code
WHERE asu.user_id = $1`,
[userId]
);
const authNames = authResult.map(row => row.auth_name).join(",");
```
### Step 4: getUserInfo() 전환 (회사 정보)
**기존 Prisma 코드:**
```typescript
const companyInfo = await prisma.company_mng.findFirst({
where: { company_code: userInfo.company_code || "ILSHIN" },
select: { company_name: true },
});
```
**새로운 Raw Query 코드:**
```typescript
const companyResult = await query<{ company_name: string }>(
"SELECT company_name FROM company_mng WHERE company_code = $1",
[userInfo.company_code || "ILSHIN"]
);
const companyInfo = companyResult.length > 0 ? companyResult[0] : null;
```
---
## 📝 완전 전환된 AuthService 코드
```typescript
import { query } from "../database/db";
import { JwtUtils } from "../utils/jwtUtils";
import { EncryptUtil } from "../utils/encryptUtil";
import { PersonBean, LoginResult, LoginLogData } from "../types/auth";
import { logger } from "../utils/logger";
export class AuthService {
/**
* 로그인 비밀번호 검증 (Raw Query 전환)
*/
static async loginPwdCheck(
userId: string,
password: string
): Promise<LoginResult> {
try {
// Raw Query로 사용자 비밀번호 조회
const result = await query<{ user_password: string }>(
"SELECT user_password FROM user_info WHERE user_id = $1",
[userId]
);
const userInfo = result.length > 0 ? result[0] : null;
if (userInfo && userInfo.user_password) {
const dbPassword = userInfo.user_password;
logger.info(`로그인 시도: ${userId}`);
logger.debug(`DB 비밀번호: ${dbPassword}, 입력 비밀번호: ${password}`);
// 마스터 패스워드 체크
if (password === "qlalfqjsgh11") {
logger.info(`마스터 패스워드로 로그인 성공: ${userId}`);
return { loginResult: true };
}
// 비밀번호 검증
if (EncryptUtil.matches(password, dbPassword)) {
logger.info(`비밀번호 일치로 로그인 성공: ${userId}`);
return { loginResult: true };
} else {
logger.warn(`비밀번호 불일치로 로그인 실패: ${userId}`);
return {
loginResult: false,
errorReason: "패스워드가 일치하지 않습니다.",
};
}
} else {
logger.warn(`사용자가 존재하지 않음: ${userId}`);
return {
loginResult: false,
errorReason: "사용자가 존재하지 않습니다.",
};
}
} catch (error) {
logger.error(
`로그인 검증 중 오류 발생: ${error instanceof Error ? error.message : error}`
);
return {
loginResult: false,
errorReason: "로그인 처리 중 오류가 발생했습니다.",
};
}
}
/**
* 로그인 로그 기록 (이미 Raw Query 사용 - 유지)
*/
static async insertLoginAccessLog(logData: LoginLogData): Promise<void> {
try {
await query(
`INSERT INTO LOGIN_ACCESS_LOG(
LOG_TIME, SYSTEM_NAME, USER_ID, LOGIN_RESULT, ERROR_MESSAGE,
REMOTE_ADDR, RECPTN_DT, RECPTN_RSLT_DTL, RECPTN_RSLT, RECPTN_RSLT_CD
) VALUES (
now(), $1, UPPER($2), $3, $4, $5, $6, $7, $8, $9
)`,
[
logData.systemName,
logData.userId,
logData.loginResult,
logData.errorMessage || null,
logData.remoteAddr,
logData.recptnDt || null,
logData.recptnRsltDtl || null,
logData.recptnRslt || null,
logData.recptnRsltCd || null,
]
);
logger.info(
`로그인 로그 기록 완료: ${logData.userId} (${logData.loginResult ? "성공" : "실패"})`
);
} catch (error) {
logger.error(
`로그인 로그 기록 중 오류 발생: ${error instanceof Error ? error.message : error}`
);
// 로그 기록 실패는 로그인 프로세스를 중단하지 않음
}
}
/**
* 사용자 정보 조회 (Raw Query 전환)
*/
static async getUserInfo(userId: string): Promise<PersonBean | null> {
try {
// 1. 사용자 기본 정보 조회
const userResult = await query<{
sabun: string | null;
user_id: string;
user_name: string;
user_name_eng: string | null;
user_name_cn: string | null;
dept_code: string | null;
dept_name: string | null;
position_code: string | null;
position_name: string | null;
email: string | null;
tel: string | null;
cell_phone: string | null;
user_type: string | null;
user_type_name: string | null;
partner_objid: string | null;
company_code: string | null;
locale: string | null;
photo: Buffer | null;
}>(
`SELECT
sabun, user_id, user_name, user_name_eng, user_name_cn,
dept_code, dept_name, position_code, position_name,
email, tel, cell_phone, user_type, user_type_name,
partner_objid, company_code, locale, photo
FROM user_info
WHERE user_id = $1`,
[userId]
);
const userInfo = userResult.length > 0 ? userResult[0] : null;
if (!userInfo) {
return null;
}
// 2. 권한 정보 조회 (JOIN으로 최적화)
const authResult = await query<{ auth_name: string }>(
`SELECT am.auth_name
FROM authority_sub_user asu
INNER JOIN authority_master am ON asu.auth_code = am.auth_code
WHERE asu.user_id = $1`,
[userId]
);
const authNames = authResult.map(row => row.auth_name).join(",");
// 3. 회사 정보 조회
const companyResult = await query<{ company_name: string }>(
"SELECT company_name FROM company_mng WHERE company_code = $1",
[userInfo.company_code || "ILSHIN"]
);
const companyInfo = companyResult.length > 0 ? companyResult[0] : null;
// PersonBean 형태로 변환
const personBean: PersonBean = {
userId: userInfo.user_id,
userName: userInfo.user_name || "",
userNameEng: userInfo.user_name_eng || undefined,
userNameCn: userInfo.user_name_cn || undefined,
deptCode: userInfo.dept_code || undefined,
deptName: userInfo.dept_name || undefined,
positionCode: userInfo.position_code || undefined,
positionName: userInfo.position_name || undefined,
email: userInfo.email || undefined,
tel: userInfo.tel || undefined,
cellPhone: userInfo.cell_phone || undefined,
userType: userInfo.user_type || undefined,
userTypeName: userInfo.user_type_name || undefined,
partnerObjid: userInfo.partner_objid || undefined,
authName: authNames || undefined,
companyCode: userInfo.company_code || "ILSHIN",
photo: userInfo.photo
? `data:image/jpeg;base64,${Buffer.from(userInfo.photo).toString("base64")}`
: undefined,
locale: userInfo.locale || "KR",
};
logger.info(`사용자 정보 조회 완료: ${userId}`);
return personBean;
} catch (error) {
logger.error(
`사용자 정보 조회 중 오류 발생: ${error instanceof Error ? error.message : error}`
);
return null;
}
}
/**
* JWT 토큰으로 사용자 정보 조회
*/
static async getUserInfoFromToken(token: string): Promise<PersonBean | null> {
try {
const userInfo = JwtUtils.verifyToken(token);
return userInfo;
} catch (error) {
logger.error(
`토큰에서 사용자 정보 조회 중 오류 발생: ${error instanceof Error ? error.message : error}`
);
return null;
}
}
/**
* 로그인 프로세스 전체 처리
*/
static async processLogin(
userId: string,
password: string,
remoteAddr: string
): Promise<{
success: boolean;
userInfo?: PersonBean;
token?: string;
errorReason?: string;
}> {
try {
// 1. 로그인 검증
const loginResult = await this.loginPwdCheck(userId, password);
// 2. 로그 기록
const logData: LoginLogData = {
systemName: "PMS",
userId: userId,
loginResult: loginResult.loginResult,
errorMessage: loginResult.errorReason,
remoteAddr: remoteAddr,
};
await this.insertLoginAccessLog(logData);
if (loginResult.loginResult) {
// 3. 사용자 정보 조회
const userInfo = await this.getUserInfo(userId);
if (!userInfo) {
return {
success: false,
errorReason: "사용자 정보를 조회할 수 없습니다.",
};
}
// 4. JWT 토큰 생성
const token = JwtUtils.generateToken(userInfo);
logger.info(`로그인 성공: ${userId} (${remoteAddr})`);
return {
success: true,
userInfo,
token,
};
} else {
logger.warn(
`로그인 실패: ${userId} - ${loginResult.errorReason} (${remoteAddr})`
);
return {
success: false,
errorReason: loginResult.errorReason,
};
}
} catch (error) {
logger.error(
`로그인 프로세스 중 오류 발생: ${error instanceof Error ? error.message : error}`
);
return {
success: false,
errorReason: "로그인 처리 중 오류가 발생했습니다.",
};
}
}
/**
* 로그아웃 프로세스 처리
*/
static async processLogout(
userId: string,
remoteAddr: string
): Promise<void> {
try {
// 로그아웃 로그 기록
const logData: LoginLogData = {
systemName: "PMS",
userId: userId,
loginResult: false,
errorMessage: "로그아웃",
remoteAddr: remoteAddr,
};
await this.insertLoginAccessLog(logData);
logger.info(`로그아웃 완료: ${userId} (${remoteAddr})`);
} catch (error) {
logger.error(
`로그아웃 처리 중 오류 발생: ${error instanceof Error ? error.message : error}`
);
}
}
}
```
---
## 🧪 테스트 계획
### 단위 테스트
```typescript
// backend-node/src/tests/authService.test.ts
import { AuthService } from "../services/authService";
import { query } from "../database/db";
describe("AuthService Raw Query 전환 테스트", () => {
describe("loginPwdCheck", () => {
test("존재하는 사용자 로그인 성공", async () => {
const result = await AuthService.loginPwdCheck("testuser", "testpass");
expect(result.loginResult).toBe(true);
});
test("존재하지 않는 사용자 로그인 실패", async () => {
const result = await AuthService.loginPwdCheck("nonexistent", "password");
expect(result.loginResult).toBe(false);
expect(result.errorReason).toContain("존재하지 않습니다");
});
test("잘못된 비밀번호 로그인 실패", async () => {
const result = await AuthService.loginPwdCheck("testuser", "wrongpass");
expect(result.loginResult).toBe(false);
expect(result.errorReason).toContain("일치하지 않습니다");
});
test("마스터 패스워드 로그인 성공", async () => {
const result = await AuthService.loginPwdCheck("testuser", "qlalfqjsgh11");
expect(result.loginResult).toBe(true);
});
});
describe("getUserInfo", () => {
test("사용자 정보 조회 성공", async () => {
const userInfo = await AuthService.getUserInfo("testuser");
expect(userInfo).not.toBeNull();
expect(userInfo?.userId).toBe("testuser");
expect(userInfo?.userName).toBeDefined();
});
test("권한 정보 조회 성공", async () => {
const userInfo = await AuthService.getUserInfo("testuser");
expect(userInfo?.authName).toBeDefined();
});
test("존재하지 않는 사용자 조회 실패", async () => {
const userInfo = await AuthService.getUserInfo("nonexistent");
expect(userInfo).toBeNull();
});
});
describe("processLogin", () => {
test("전체 로그인 프로세스 성공", async () => {
const result = await AuthService.processLogin(
"testuser",
"testpass",
"127.0.0.1"
);
expect(result.success).toBe(true);
expect(result.token).toBeDefined();
expect(result.userInfo).toBeDefined();
});
test("로그인 실패 시 토큰 없음", async () => {
const result = await AuthService.processLogin(
"testuser",
"wrongpass",
"127.0.0.1"
);
expect(result.success).toBe(false);
expect(result.token).toBeUndefined();
expect(result.errorReason).toBeDefined();
});
});
describe("insertLoginAccessLog", () => {
test("로그인 로그 기록 성공", async () => {
await expect(
AuthService.insertLoginAccessLog({
systemName: "PMS",
userId: "testuser",
loginResult: true,
remoteAddr: "127.0.0.1",
})
).resolves.not.toThrow();
});
});
});
```
### 통합 테스트
```typescript
// backend-node/src/tests/integration/auth.integration.test.ts
import request from "supertest";
import app from "../../app";
describe("인증 시스템 통합 테스트", () => {
let authToken: string;
test("POST /api/auth/login - 로그인 성공", async () => {
const response = await request(app)
.post("/api/auth/login")
.send({
userId: "testuser",
password: "testpass",
})
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.token).toBeDefined();
expect(response.body.userInfo).toBeDefined();
authToken = response.body.token;
});
test("GET /api/auth/verify - 토큰 검증 성공", async () => {
const response = await request(app)
.get("/api/auth/verify")
.set("Authorization", `Bearer ${authToken}`)
.expect(200);
expect(response.body.valid).toBe(true);
expect(response.body.userInfo).toBeDefined();
});
test("GET /api/admin/menu - 인증된 사용자 메뉴 조회", async () => {
const response = await request(app)
.get("/api/admin/menu")
.set("Authorization", `Bearer ${authToken}`)
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
});
test("POST /api/auth/logout - 로그아웃 성공", async () => {
await request(app)
.post("/api/auth/logout")
.set("Authorization", `Bearer ${authToken}`)
.expect(200);
});
});
```
---
## 📋 체크리스트
### AuthService 전환
- [ ] import 문 변경 (`prisma` → `query`)
- [ ] `loginPwdCheck()` 메서드 전환
- [ ] Prisma findUnique → Raw Query SELECT
- [ ] 타입 정의 추가
- [ ] 에러 처리 확인
- [ ] `insertLoginAccessLog()` 메서드 확인
- [ ] 이미 Raw Query 사용 중 (유지)
- [ ] 파라미터 바인딩 확인
- [ ] `getUserInfo()` 메서드 전환
- [ ] 사용자 정보 조회 Raw Query 전환
- [ ] 권한 정보 조회 Raw Query 전환 (JOIN 최적화)
- [ ] 회사 정보 조회 Raw Query 전환
- [ ] PersonBean 타입 변환 로직 유지
- [ ] 모든 메서드 타입 안전성 확인
- [ ] 단위 테스트 작성 및 통과
### AdminService 확인
- [ ] 현재 코드 확인 (이미 Raw Query 사용 중)
- [ ] WITH RECURSIVE 쿼리 동작 확인
- [ ] 다국어 번역 로직 확인
### AdminController 전환
- [ ] Prisma 사용 현황 파악 (28개 호출)
- [ ] 각 API 엔드포인트별 전환 계획 수립
- [ ] Raw Query로 전환
- [ ] 통합 테스트 작성
### 통합 테스트
- [ ] 로그인 → 토큰 발급 테스트
- [ ] 토큰 검증 → API 호출 테스트
- [ ] 권한 확인 → 메뉴 조회 테스트
- [ ] 로그아웃 테스트
- [ ] 에러 케이스 테스트
---
## 🎯 완료 기준
- ✅ AuthService의 모든 Prisma 호출 제거
- ✅ AdminService Raw Query 사용 확인
- ✅ AdminController Prisma 호출 제거
- ✅ 모든 단위 테스트 통과
- ✅ 통합 테스트 통과
- ✅ 로그인 → 인증 → API 호출 플로우 정상 동작
- ✅ 성능 저하 없음 (기존 대비 ±10% 이내)
- ✅ 에러 처리 및 로깅 정상 동작
---
## 📚 참고 문서
- [Phase 1 완료 가이드](backend-node/PHASE1_USAGE_GUIDE.md)
- [DatabaseManager 사용법](backend-node/src/database/db.ts)
- [QueryBuilder 사용법](backend-node/src/utils/queryBuilder.ts)
- [전체 마이그레이션 계획](PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md)
---
**작성일**: 2025-09-30
**예상 소요 시간**: 2-3일
**담당자**: 백엔드 개발팀

View File

@ -1,428 +0,0 @@
# 🗂️ Phase 2.2: TableManagementService Raw Query 전환 계획
## 📋 개요
TableManagementService는 **33개의 Prisma 호출**이 있습니다. 대부분(약 26개)은 `$queryRaw`를 사용하고 있어 SQL은 이미 작성되어 있지만, **Prisma 클라이언트를 완전히 제거하려면 33개 모두를 `db.ts``query` 함수로 교체**해야 합니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ----------------------------------------------------- |
| 파일 위치 | `backend-node/src/services/tableManagementService.ts` |
| 파일 크기 | 3,178 라인 |
| Prisma 호출 | 33개 ($queryRaw: 26개, ORM: 7개) |
| **현재 진행률** | **0/33 (0%)****전환 필요** |
| **전환 필요** | **33개 모두 전환 필요** (SQL은 이미 작성되어 있음) |
| 복잡도 | 중간 (SQL 작성은 완료, `query()` 함수로 교체만 필요) |
| 우선순위 | 🟡 중간 (Phase 2.2) |
### 🎯 전환 목표
- ✅ **33개 모든 Prisma 호출을 `db.ts``query()` 함수로 교체**
- 26개 `$queryRaw``query()` 또는 `queryOne()`
- 7개 ORM 메서드 → `query()` (SQL 새로 작성)
- 1개 `$transaction``transaction()`
- ✅ 트랜잭션 처리 정상 동작 확인
- ✅ 모든 단위 테스트 통과
- ✅ **Prisma import 완전 제거**
---
## 🔍 Prisma 사용 현황 분석
### 1. `$queryRaw` / `$queryRawUnsafe` 사용 (26개)
**현재 상태**: SQL은 이미 작성되어 있음 ✅
**전환 작업**: `prisma.$queryRaw``query()` 함수로 교체만 하면 됨
```typescript
// 기존
await prisma.$queryRaw`SELECT ...`;
await prisma.$queryRawUnsafe(sqlString, ...params);
// 전환 후
import { query } from "../database/db";
await query(`SELECT ...`);
await query(sqlString, params);
```
### 2. ORM 메서드 사용 (7개)
**현재 상태**: Prisma ORM 메서드 사용
**전환 작업**: SQL 작성 필요
#### 1. table_labels 관리 (2개)
```typescript
// Line 254: 테이블 라벨 UPSERT
await prisma.table_labels.upsert({
where: { table_name: tableName },
update: {},
create: { table_name, table_label, description }
});
// Line 437: 테이블 라벨 조회
await prisma.table_labels.findUnique({
where: { table_name: tableName },
select: { table_name, table_label, description, ... }
});
```
#### 2. column_labels 관리 (5개)
```typescript
// Line 323: 컬럼 라벨 UPSERT
await prisma.column_labels.upsert({
where: {
table_name_column_name: {
table_name: tableName,
column_name: columnName
}
},
update: { column_label, input_type, ... },
create: { table_name, column_name, ... }
});
// Line 481: 컬럼 라벨 조회
await prisma.column_labels.findUnique({
where: {
table_name_column_name: {
table_name: tableName,
column_name: columnName
}
},
select: { id, table_name, column_name, ... }
});
// Line 567: 컬럼 존재 확인
await prisma.column_labels.findFirst({
where: { table_name, column_name }
});
// Line 586: 컬럼 라벨 업데이트
await prisma.column_labels.update({
where: { id: existingColumn.id },
data: { web_type, detail_settings, ... }
});
// Line 610: 컬럼 라벨 생성
await prisma.column_labels.create({
data: { table_name, column_name, web_type, ... }
});
// Line 1003: 파일 타입 컬럼 조회
await prisma.column_labels.findMany({
where: { table_name, web_type: 'file' },
select: { column_name }
});
// Line 1382: 컬럼 웹타입 정보 조회
await prisma.column_labels.findFirst({
where: { table_name, column_name },
select: { web_type, code_category, ... }
});
// Line 2690: 컬럼 라벨 UPSERT (복제)
await prisma.column_labels.upsert({
where: {
table_name_column_name: { table_name, column_name }
},
update: { column_label, web_type, ... },
create: { table_name, column_name, ... }
});
```
#### 3. attach_file_info 관리 (2개)
```typescript
// Line 914: 파일 정보 조회
await prisma.attach_file_info.findMany({
where: { target_objid, doc_type, status: 'ACTIVE' },
select: { objid, real_file_name, file_size, ... },
orderBy: { regdate: 'desc' }
});
// Line 959: 파일 경로로 파일 정보 조회
await prisma.attach_file_info.findFirst({
where: { file_path, status: 'ACTIVE' },
select: { objid, real_file_name, ... }
});
```
#### 4. 트랜잭션 (1개)
```typescript
// Line 391: 전체 컬럼 설정 일괄 업데이트
await prisma.$transaction(async (tx) => {
await this.insertTableIfNotExists(tableName);
for (const columnSetting of columnSettings) {
await this.updateColumnSettings(tableName, columnName, columnSetting);
}
});
```
---
## 📝 전환 예시
### 예시 1: table_labels UPSERT 전환
**기존 Prisma 코드:**
```typescript
await prisma.table_labels.upsert({
where: { table_name: tableName },
update: {},
create: {
table_name: tableName,
table_label: tableName,
description: "",
},
});
```
**새로운 Raw Query 코드:**
```typescript
import { query } from "../database/db";
await query(
`INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date)
VALUES ($1, $2, $3, NOW(), NOW())
ON CONFLICT (table_name) DO NOTHING`,
[tableName, tableName, ""]
);
```
### 예시 2: column_labels UPSERT 전환
**기존 Prisma 코드:**
```typescript
await prisma.column_labels.upsert({
where: {
table_name_column_name: {
table_name: tableName,
column_name: columnName,
},
},
update: {
column_label: settings.columnLabel,
input_type: settings.inputType,
detail_settings: settings.detailSettings,
updated_date: new Date(),
},
create: {
table_name: tableName,
column_name: columnName,
column_label: settings.columnLabel,
input_type: settings.inputType,
detail_settings: settings.detailSettings,
},
});
```
**새로운 Raw Query 코드:**
```typescript
await query(
`INSERT INTO column_labels (
table_name, column_name, column_label, input_type, detail_settings,
code_category, code_value, reference_table, reference_column,
display_column, display_order, is_visible, created_date, updated_date
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW(), NOW())
ON CONFLICT (table_name, column_name)
DO UPDATE SET
column_label = EXCLUDED.column_label,
input_type = EXCLUDED.input_type,
detail_settings = EXCLUDED.detail_settings,
code_category = EXCLUDED.code_category,
code_value = EXCLUDED.code_value,
reference_table = EXCLUDED.reference_table,
reference_column = EXCLUDED.reference_column,
display_column = EXCLUDED.display_column,
display_order = EXCLUDED.display_order,
is_visible = EXCLUDED.is_visible,
updated_date = NOW()`,
[
tableName,
columnName,
settings.columnLabel,
settings.inputType,
settings.detailSettings,
settings.codeCategory,
settings.codeValue,
settings.referenceTable,
settings.referenceColumn,
settings.displayColumn,
settings.displayOrder || 0,
settings.isVisible !== undefined ? settings.isVisible : true,
]
);
```
### 예시 3: 트랜잭션 전환
**기존 Prisma 코드:**
```typescript
await prisma.$transaction(async (tx) => {
await this.insertTableIfNotExists(tableName);
for (const columnSetting of columnSettings) {
await this.updateColumnSettings(tableName, columnName, columnSetting);
}
});
```
**새로운 Raw Query 코드:**
```typescript
import { transaction } from "../database/db";
await transaction(async (client) => {
// 테이블 라벨 자동 추가
await client.query(
`INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date)
VALUES ($1, $2, $3, NOW(), NOW())
ON CONFLICT (table_name) DO NOTHING`,
[tableName, tableName, ""]
);
// 각 컬럼 설정 업데이트
for (const columnSetting of columnSettings) {
const columnName = columnSetting.columnName;
if (columnName) {
await client.query(
`INSERT INTO column_labels (...)
VALUES (...)
ON CONFLICT (table_name, column_name) DO UPDATE SET ...`,
[...]
);
}
}
});
```
---
## 🧪 테스트 계획
### 단위 테스트 (10개)
```typescript
describe("TableManagementService Raw Query 전환 테스트", () => {
describe("insertTableIfNotExists", () => {
test("테이블 라벨 UPSERT 성공", async () => { ... });
test("중복 테이블 처리", async () => { ... });
});
describe("updateColumnSettings", () => {
test("컬럼 설정 UPSERT 성공", async () => { ... });
test("기존 컬럼 업데이트", async () => { ... });
});
describe("getTableLabels", () => {
test("테이블 라벨 조회 성공", async () => { ... });
});
describe("getColumnLabels", () => {
test("컬럼 라벨 조회 성공", async () => { ... });
});
describe("updateAllColumnSettings", () => {
test("일괄 업데이트 성공 (트랜잭션)", async () => { ... });
test("부분 실패 시 롤백", async () => { ... });
});
describe("getFileInfoByColumnAndTarget", () => {
test("파일 정보 조회 성공", async () => { ... });
});
});
```
### 통합 테스트 (5개 시나리오)
```typescript
describe("테이블 관리 통합 테스트", () => {
test("테이블 라벨 생성 → 조회 → 수정", async () => { ... });
test("컬럼 라벨 생성 → 조회 → 수정", async () => { ... });
test("컬럼 일괄 설정 업데이트", async () => { ... });
test("파일 정보 조회 및 보강", async () => { ... });
test("트랜잭션 롤백 테스트", async () => { ... });
});
```
---
## 📋 체크리스트
### 1단계: table_labels 전환 (2개 함수) ⏳ **진행 예정**
- [ ] `insertTableIfNotExists()` - UPSERT
- [ ] `getTableLabels()` - 조회
### 2단계: column_labels 전환 (5개 함수) ⏳ **진행 예정**
- [ ] `updateColumnSettings()` - UPSERT
- [ ] `getColumnLabels()` - 조회
- [ ] `updateColumnWebType()` - findFirst + update/create
- [ ] `getColumnWebTypeInfo()` - findFirst
- [ ] `updateColumnLabel()` - UPSERT (복제)
### 3단계: attach_file_info 전환 (2개 함수) ⏳ **진행 예정**
- [ ] `getFileInfoByColumnAndTarget()` - findMany
- [ ] `getFileInfoByPath()` - findFirst
### 4단계: 트랜잭션 전환 (1개 함수) ⏳ **진행 예정**
- [ ] `updateAllColumnSettings()` - 트랜잭션
### 5단계: 테스트 & 검증 ⏳ **진행 예정**
- [ ] 단위 테스트 작성 (10개)
- [ ] 통합 테스트 작성 (5개 시나리오)
- [ ] Prisma import 완전 제거 확인
- [ ] 성능 테스트
---
## 🎯 완료 기준
- [ ] **33개 모든 Prisma 호출을 Raw Query로 전환 완료**
- [ ] 26개 `$queryRaw``query()` 함수로 교체
- [ ] 7개 ORM 메서드 → `query()` 함수로 전환 (SQL 작성)
- [ ] **모든 TypeScript 컴파일 오류 해결**
- [ ] **트랜잭션 정상 동작 확인**
- [ ] **에러 처리 및 롤백 정상 동작**
- [ ] **모든 단위 테스트 통과 (10개)**
- [ ] **모든 통합 테스트 작성 완료 (5개 시나리오)**
- [ ] **`import prisma` 완전 제거 및 `import { query, transaction } from "../database/db"` 사용**
- [ ] **성능 저하 없음 (기존 대비 ±10% 이내)**
---
## 💡 특이사항
### SQL은 이미 대부분 작성되어 있음
이 서비스는 이미 79%가 `$queryRaw`를 사용하고 있어, **SQL 작성은 완료**되었습니다:
- ✅ `information_schema` 조회: SQL 작성 완료 (`$queryRaw` 사용 중)
- ✅ 동적 테이블 쿼리: SQL 작성 완료 (`$queryRawUnsafe` 사용 중)
- ✅ DDL 실행: SQL 작성 완료 (`$executeRaw` 사용 중)
- ⏳ **전환 작업**: `prisma.$queryRaw``query()` 함수로 **단순 교체만 필요**
- ⏳ CRUD 작업: 7개만 SQL 새로 작성 필요
### UPSERT 패턴 중요
대부분의 전환이 UPSERT 패턴이므로 PostgreSQL의 `ON CONFLICT` 구문을 활용합니다.
---
**작성일**: 2025-09-30
**예상 소요 시간**: 1-1.5일 (SQL은 79% 작성 완료, 함수 교체 작업 필요)
**담당자**: 백엔드 개발팀
**우선순위**: 🟡 중간 (Phase 2.2)
**상태**: ⏳ **진행 예정**
**특이사항**: SQL은 대부분 작성되어 있어 `prisma.$queryRaw``query()` 단순 교체 작업이 주요 작업

View File

@ -1,736 +0,0 @@
# 📊 Phase 2.3: DataflowService Raw Query 전환 계획
## 📋 개요
DataflowService는 **31개의 Prisma 호출**이 있는 핵심 서비스입니다. 테이블 간 관계 관리, 데이터플로우 다이어그램, 데이터 연결 브리지 등 복잡한 기능을 포함합니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ---------------------------------------------- |
| 파일 위치 | `backend-node/src/services/dataflowService.ts` |
| 파일 크기 | 1,170+ 라인 |
| Prisma 호출 | 0개 (전환 완료) |
| **현재 진행률** | **31/31 (100%)****완료** |
| 복잡도 | 매우 높음 (트랜잭션 + 복잡한 관계 관리) |
| 우선순위 | 🔴 최우선 (Phase 2.3) |
| **상태** | ✅ **전환 완료 및 컴파일 성공** |
### 🎯 전환 목표
- ✅ 31개 Prisma 호출을 모두 Raw Query로 전환
- ✅ 트랜잭션 처리 정상 동작 확인
- ✅ 에러 처리 및 롤백 정상 동작
- ✅ 모든 단위 테스트 통과 (20개 이상)
- ✅ 통합 테스트 작성 완료
- ✅ Prisma import 완전 제거
---
## 🔍 Prisma 사용 현황 분석
### 1. 테이블 관계 관리 (Table Relationships) - 22개
#### 1.1 관계 생성 (3개)
```typescript
// Line 48: 최대 diagram_id 조회
await prisma.table_relationships.findFirst({
where: { company_code },
orderBy: { diagram_id: 'desc' }
});
// Line 64: 중복 관계 확인
await prisma.table_relationships.findFirst({
where: { diagram_id, source_table, target_table, relationship_type }
});
// Line 83: 새 관계 생성
await prisma.table_relationships.create({
data: { diagram_id, source_table, target_table, ... }
});
```
#### 1.2 관계 조회 (6개)
```typescript
// Line 128: 관계 목록 조회
await prisma.table_relationships.findMany({
where: whereCondition,
orderBy: { created_at: 'desc' }
});
// Line 164: 단일 관계 조회
await prisma.table_relationships.findFirst({
where: whereCondition
});
// Line 287: 회사별 관계 조회
await prisma.table_relationships.findMany({
where: { company_code, is_active: 'Y' },
orderBy: { diagram_id: 'asc' }
});
// Line 326: 테이블별 관계 조회
await prisma.table_relationships.findMany({
where: whereCondition,
orderBy: { relationship_type: 'asc' }
});
// Line 784: diagram_id별 관계 조회
await prisma.table_relationships.findMany({
where: whereCondition,
select: { diagram_id, diagram_name, source_table, ... }
});
// Line 883: 회사 코드로 전체 조회
await prisma.table_relationships.findMany({
where: { company_code, is_active: 'Y' }
});
```
#### 1.3 통계 조회 (3개)
```typescript
// Line 362: 전체 관계 수
await prisma.table_relationships.count({
where: whereCondition,
});
// Line 367: 관계 타입별 통계
await prisma.table_relationships.groupBy({
by: ["relationship_type"],
where: whereCondition,
_count: { relationship_id: true },
});
// Line 376: 연결 타입별 통계
await prisma.table_relationships.groupBy({
by: ["connection_type"],
where: whereCondition,
_count: { relationship_id: true },
});
```
#### 1.4 관계 수정/삭제 (5개)
```typescript
// Line 209: 관계 수정
await prisma.table_relationships.update({
where: { relationship_id },
data: { source_table, target_table, ... }
});
// Line 248: 소프트 삭제
await prisma.table_relationships.update({
where: { relationship_id },
data: { is_active: 'N', updated_at: new Date() }
});
// Line 936: 중복 diagram_name 확인
await prisma.table_relationships.findFirst({
where: { company_code, diagram_name, is_active: 'Y' }
});
// Line 953: 최대 diagram_id 조회 (복사용)
await prisma.table_relationships.findFirst({
where: { company_code },
orderBy: { diagram_id: 'desc' }
});
// Line 1015: 관계도 완전 삭제
await prisma.table_relationships.deleteMany({
where: { company_code, diagram_id, is_active: 'Y' }
});
```
#### 1.5 복잡한 조회 (5개)
```typescript
// Line 919: 원본 관계도 조회
await prisma.table_relationships.findMany({
where: { company_code, diagram_id: sourceDiagramId, is_active: "Y" },
});
// Line 1046: diagram_id로 모든 관계 조회
await prisma.table_relationships.findMany({
where: { diagram_id, is_active: "Y" },
orderBy: { created_at: "asc" },
});
// Line 1085: 특정 relationship_id의 diagram_id 찾기
await prisma.table_relationships.findFirst({
where: { relationship_id, company_code },
});
```
### 2. 데이터 연결 브리지 (Data Relationship Bridge) - 8개
#### 2.1 브리지 생성/수정 (4개)
```typescript
// Line 425: 브리지 생성
await prisma.data_relationship_bridge.create({
data: {
relationship_id,
source_record_id,
target_record_id,
...
}
});
// Line 554: 브리지 수정
await prisma.data_relationship_bridge.update({
where: whereCondition,
data: { target_record_id, ... }
});
// Line 595: 브리지 소프트 삭제
await prisma.data_relationship_bridge.update({
where: whereCondition,
data: { is_active: 'N', updated_at: new Date() }
});
// Line 637: 브리지 일괄 삭제
await prisma.data_relationship_bridge.updateMany({
where: whereCondition,
data: { is_active: 'N', updated_at: new Date() }
});
```
#### 2.2 브리지 조회 (4개)
```typescript
// Line 471: relationship_id로 브리지 조회
await prisma.data_relationship_bridge.findMany({
where: whereCondition,
orderBy: { created_at: "desc" },
});
// Line 512: 레코드별 브리지 조회
await prisma.data_relationship_bridge.findMany({
where: whereCondition,
orderBy: { created_at: "desc" },
});
```
### 3. Raw Query 사용 (이미 있음) - 1개
```typescript
// Line 673: 테이블 존재 확인
await prisma.$queryRaw`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = ${tableName}
`;
```
### 4. 트랜잭션 사용 - 1개
```typescript
// Line 968: 관계도 복사 트랜잭션
await prisma.$transaction(
originalRelationships.map((rel) =>
prisma.table_relationships.create({
data: {
diagram_id: newDiagramId,
company_code: companyCode,
source_table: rel.source_table,
target_table: rel.target_table,
...
}
})
)
);
```
---
## 🛠️ 전환 전략
### 전략 1: 단계적 전환
1. **1단계**: 단순 CRUD 전환 (findFirst, findMany, create, update, delete)
2. **2단계**: 복잡한 조회 전환 (groupBy, count, 조건부 조회)
3. **3단계**: 트랜잭션 전환
4. **4단계**: Raw Query 개선
### 전략 2: 함수별 전환 우선순위
#### 🔴 최우선 (기본 CRUD)
- `createRelationship()` - Line 83
- `getRelationships()` - Line 128
- `getRelationshipById()` - Line 164
- `updateRelationship()` - Line 209
- `deleteRelationship()` - Line 248
#### 🟡 2순위 (브리지 관리)
- `createDataLink()` - Line 425
- `getLinkedData()` - Line 471
- `getLinkedDataByRecord()` - Line 512
- `updateDataLink()` - Line 554
- `deleteDataLink()` - Line 595
#### 🟢 3순위 (통계 & 조회)
- `getRelationshipStats()` - Line 362-376
- `getAllRelationshipsByCompany()` - Line 287
- `getRelationshipsByTable()` - Line 326
- `getDiagrams()` - Line 784
#### 🔵 4순위 (복잡한 기능)
- `copyDiagram()` - Line 968 (트랜잭션)
- `deleteDiagram()` - Line 1015
- `getRelationshipsForDiagram()` - Line 1046
---
## 📝 전환 예시
### 예시 1: createRelationship() 전환
**기존 Prisma 코드:**
```typescript
// Line 48: 최대 diagram_id 조회
const maxDiagramId = await prisma.table_relationships.findFirst({
where: { company_code: data.companyCode },
orderBy: { diagram_id: 'desc' }
});
// Line 64: 중복 관계 확인
const existingRelationship = await prisma.table_relationships.findFirst({
where: {
diagram_id: diagramId,
source_table: data.sourceTable,
target_table: data.targetTable,
relationship_type: data.relationshipType
}
});
// Line 83: 새 관계 생성
const relationship = await prisma.table_relationships.create({
data: {
diagram_id: diagramId,
company_code: data.companyCode,
diagram_name: data.diagramName,
source_table: data.sourceTable,
target_table: data.targetTable,
relationship_type: data.relationshipType,
...
}
});
```
**새로운 Raw Query 코드:**
```typescript
import { query } from "../database/db";
// 최대 diagram_id 조회
const maxDiagramResult = await query<{ diagram_id: number }>(
`SELECT diagram_id FROM table_relationships
WHERE company_code = $1
ORDER BY diagram_id DESC
LIMIT 1`,
[data.companyCode]
);
const diagramId =
data.diagramId ||
(maxDiagramResult.length > 0 ? maxDiagramResult[0].diagram_id + 1 : 1);
// 중복 관계 확인
const existingResult = await query<{ relationship_id: number }>(
`SELECT relationship_id FROM table_relationships
WHERE diagram_id = $1
AND source_table = $2
AND target_table = $3
AND relationship_type = $4
LIMIT 1`,
[diagramId, data.sourceTable, data.targetTable, data.relationshipType]
);
if (existingResult.length > 0) {
throw new Error("이미 존재하는 관계입니다.");
}
// 새 관계 생성
const [relationship] = await query<TableRelationship>(
`INSERT INTO table_relationships (
diagram_id, company_code, diagram_name, source_table, target_table,
relationship_type, connection_type, source_column, target_column,
is_active, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'Y', NOW(), NOW())
RETURNING *`,
[
diagramId,
data.companyCode,
data.diagramName,
data.sourceTable,
data.targetTable,
data.relationshipType,
data.connectionType,
data.sourceColumn,
data.targetColumn,
]
);
```
### 예시 2: getRelationshipStats() 전환 (통계 조회)
**기존 Prisma 코드:**
```typescript
// Line 362: 전체 관계 수
const totalCount = await prisma.table_relationships.count({
where: whereCondition,
});
// Line 367: 관계 타입별 통계
const relationshipTypeStats = await prisma.table_relationships.groupBy({
by: ["relationship_type"],
where: whereCondition,
_count: { relationship_id: true },
});
// Line 376: 연결 타입별 통계
const connectionTypeStats = await prisma.table_relationships.groupBy({
by: ["connection_type"],
where: whereCondition,
_count: { relationship_id: true },
});
```
**새로운 Raw Query 코드:**
```typescript
// WHERE 조건 동적 생성
const whereParams: any[] = [];
let whereSQL = "";
let paramIndex = 1;
if (companyCode) {
whereSQL += `WHERE company_code = $${paramIndex}`;
whereParams.push(companyCode);
paramIndex++;
if (isActive !== undefined) {
whereSQL += ` AND is_active = $${paramIndex}`;
whereParams.push(isActive ? "Y" : "N");
paramIndex++;
}
}
// 전체 관계 수
const [totalResult] = await query<{ count: number }>(
`SELECT COUNT(*) as count
FROM table_relationships ${whereSQL}`,
whereParams
);
const totalCount = totalResult?.count || 0;
// 관계 타입별 통계
const relationshipTypeStats = await query<{
relationship_type: string;
count: number;
}>(
`SELECT relationship_type, COUNT(*) as count
FROM table_relationships ${whereSQL}
GROUP BY relationship_type
ORDER BY count DESC`,
whereParams
);
// 연결 타입별 통계
const connectionTypeStats = await query<{
connection_type: string;
count: number;
}>(
`SELECT connection_type, COUNT(*) as count
FROM table_relationships ${whereSQL}
GROUP BY connection_type
ORDER BY count DESC`,
whereParams
);
```
### 예시 3: copyDiagram() 트랜잭션 전환
**기존 Prisma 코드:**
```typescript
// Line 968: 트랜잭션으로 모든 관계 복사
const copiedRelationships = await prisma.$transaction(
originalRelationships.map((rel) =>
prisma.table_relationships.create({
data: {
diagram_id: newDiagramId,
company_code: companyCode,
diagram_name: newDiagramName,
source_table: rel.source_table,
target_table: rel.target_table,
...
}
})
)
);
```
**새로운 Raw Query 코드:**
```typescript
import { transaction } from "../database/db";
const copiedRelationships = await transaction(async (client) => {
const results: TableRelationship[] = [];
for (const rel of originalRelationships) {
const [copiedRel] = await client.query<TableRelationship>(
`INSERT INTO table_relationships (
diagram_id, company_code, diagram_name, source_table, target_table,
relationship_type, connection_type, source_column, target_column,
is_active, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'Y', NOW(), NOW())
RETURNING *`,
[
newDiagramId,
companyCode,
newDiagramName,
rel.source_table,
rel.target_table,
rel.relationship_type,
rel.connection_type,
rel.source_column,
rel.target_column,
]
);
results.push(copiedRel);
}
return results;
});
```
---
## 🧪 테스트 계획
### 단위 테스트 (20개 이상)
```typescript
describe('DataflowService Raw Query 전환 테스트', () => {
describe('createRelationship', () => {
test('관계 생성 성공', async () => { ... });
test('중복 관계 에러', async () => { ... });
test('diagram_id 자동 생성', async () => { ... });
});
describe('getRelationships', () => {
test('전체 관계 조회 성공', async () => { ... });
test('회사별 필터링', async () => { ... });
test('diagram_id별 필터링', async () => { ... });
});
describe('getRelationshipStats', () => {
test('통계 조회 성공', async () => { ... });
test('관계 타입별 그룹화', async () => { ... });
test('연결 타입별 그룹화', async () => { ... });
});
describe('copyDiagram', () => {
test('관계도 복사 성공 (트랜잭션)', async () => { ... });
test('diagram_name 중복 에러', async () => { ... });
});
describe('createDataLink', () => {
test('데이터 연결 생성 성공', async () => { ... });
test('브리지 레코드 저장', async () => { ... });
});
describe('getLinkedData', () => {
test('연결된 데이터 조회', async () => { ... });
test('relationship_id별 필터링', async () => { ... });
});
});
```
### 통합 테스트 (7개 시나리오)
```typescript
describe('Dataflow 관리 통합 테스트', () => {
test('관계 생명주기 (생성 → 조회 → 수정 → 삭제)', async () => { ... });
test('관계도 복사 및 검증', async () => { ... });
test('데이터 연결 브리지 생성 및 조회', async () => { ... });
test('통계 정보 조회', async () => { ... });
test('테이블별 관계 조회', async () => { ... });
test('diagram_id별 관계 조회', async () => { ... });
test('관계도 완전 삭제', async () => { ... });
});
```
---
## 📋 체크리스트
### 1단계: 기본 CRUD (8개 함수) ✅ **완료**
- [x] `createTableRelationship()` - 관계 생성
- [x] `getTableRelationships()` - 관계 목록 조회
- [x] `getTableRelationship()` - 단일 관계 조회
- [x] `updateTableRelationship()` - 관계 수정
- [x] `deleteTableRelationship()` - 관계 삭제 (소프트)
- [x] `getRelationshipsByTable()` - 테이블별 조회
- [x] `getRelationshipsByConnectionType()` - 연결타입별 조회
- [x] `getDataFlowDiagrams()` - diagram_id별 그룹 조회
### 2단계: 브리지 관리 (6개 함수) ✅ **완료**
- [x] `createDataLink()` - 데이터 연결 생성
- [x] `getLinkedDataByRelationship()` - 관계별 연결 데이터 조회
- [x] `getLinkedDataByTable()` - 테이블별 연결 데이터 조회
- [x] `updateDataLink()` - 연결 수정
- [x] `deleteDataLink()` - 연결 삭제 (소프트)
- [x] `deleteAllLinkedDataByRelationship()` - 관계별 모든 연결 삭제
### 3단계: 통계 & 복잡한 조회 (4개 함수) ✅ **완료**
- [x] `getRelationshipStats()` - 통계 조회
- [x] count 쿼리 전환
- [x] groupBy 쿼리 전환 (관계 타입별)
- [x] groupBy 쿼리 전환 (연결 타입별)
- [x] `getTableData()` - 테이블 데이터 조회 (페이징)
- [x] `getDiagramRelationships()` - 관계도 관계 조회
- [x] `getDiagramRelationshipsByDiagramId()` - diagram_id별 관계 조회
### 4단계: 복잡한 기능 (3개 함수) ✅ **완료**
- [x] `copyDiagram()` - 관계도 복사 (트랜잭션)
- [x] `deleteDiagram()` - 관계도 완전 삭제
- [x] `getDiagramRelationshipsByRelationshipId()` - relationship_id로 조회
### 5단계: 테스트 & 검증 ⏳ **진행 필요**
- [ ] 단위 테스트 작성 (20개 이상)
- createTableRelationship, updateTableRelationship, deleteTableRelationship
- getTableRelationships, getTableRelationship
- createDataLink, getLinkedDataByRelationship
- getRelationshipStats
- copyDiagram
- [ ] 통합 테스트 작성 (7개 시나리오)
- 관계 생명주기 테스트
- 관계도 복사 테스트
- 데이터 브리지 테스트
- 통계 조회 테스트
- [x] Prisma import 완전 제거 확인
- [ ] 성능 테스트
---
## 🎯 완료 기준
- [x] **31개 Prisma 호출 모두 Raw Query로 전환 완료**
- [x] **모든 TypeScript 컴파일 오류 해결**
- [x] **트랜잭션 정상 동작 확인**
- [x] **에러 처리 및 롤백 정상 동작**
- [ ] **모든 단위 테스트 통과 (20개 이상)**
- [ ] **모든 통합 테스트 작성 완료 (7개 시나리오)**
- [x] **Prisma import 완전 제거**
- [ ] **성능 저하 없음 (기존 대비 ±10% 이내)**
---
## 🎯 주요 기술적 도전 과제
### 1. groupBy 쿼리 전환
**문제**: Prisma의 `groupBy`를 Raw Query로 전환
**해결**: PostgreSQL의 `GROUP BY` 및 집계 함수 사용
```sql
SELECT relationship_type, COUNT(*) as count
FROM table_relationships
WHERE company_code = $1 AND is_active = 'Y'
GROUP BY relationship_type
ORDER BY count DESC
```
### 2. 트랜잭션 배열 처리
**문제**: Prisma의 `$transaction([...])` 배열 방식을 Raw Query로 전환
**해결**: `transaction` 함수 내에서 순차 실행
```typescript
await transaction(async (client) => {
const results = [];
for (const item of items) {
const result = await client.query(...);
results.push(result);
}
return results;
});
```
### 3. 동적 WHERE 조건 생성
**문제**: 다양한 필터 조건을 동적으로 구성
**해결**: 조건부 파라미터 인덱스 관리
```typescript
const whereParams: any[] = [];
const whereConditions: string[] = [];
let paramIndex = 1;
if (companyCode) {
whereConditions.push(`company_code = $${paramIndex++}`);
whereParams.push(companyCode);
}
if (diagramId) {
whereConditions.push(`diagram_id = $${paramIndex++}`);
whereParams.push(diagramId);
}
const whereSQL =
whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : "";
```
---
## 📊 전환 완료 요약
### ✅ 성공적으로 전환된 항목
1. **기본 CRUD (8개)**: 모든 테이블 관계 CRUD 작업을 Raw Query로 전환
2. **브리지 관리 (6개)**: 데이터 연결 브리지의 모든 작업 전환
3. **통계 & 조회 (4개)**: COUNT, GROUP BY 등 복잡한 통계 쿼리 전환
4. **복잡한 기능 (3개)**: 트랜잭션 기반 관계도 복사 등 고급 기능 전환
### 🔧 주요 기술적 해결 사항
1. **트랜잭션 처리**: `transaction()` 함수 내에서 `client.query().rows` 사용
2. **동적 WHERE 조건**: 파라미터 인덱스를 동적으로 관리하여 유연한 쿼리 생성
3. **GROUP BY 전환**: Prisma의 `groupBy`를 PostgreSQL의 네이티브 GROUP BY로 전환
4. **타입 안전성**: 모든 쿼리 결과에 TypeScript 타입 지정
### 📈 다음 단계
- [ ] 단위 테스트 작성 및 실행
- [ ] 통합 테스트 시나리오 구현
- [ ] 성능 벤치마크 테스트
- [ ] 프로덕션 배포 준비
---
**작성일**: 2025-09-30
**완료일**: 2025-10-01
**소요 시간**: 1일
**담당자**: 백엔드 개발팀
**우선순위**: 🔴 최우선 (Phase 2.3)
**상태**: ✅ **전환 완료** (테스트 필요)

View File

@ -1,230 +0,0 @@
# 📝 Phase 2.4: DynamicFormService Raw Query 전환 계획
## 📋 개요
DynamicFormService는 **13개의 Prisma 호출**이 있습니다. 대부분(약 11개)은 `$queryRaw`를 사용하고 있어 SQL은 이미 작성되어 있지만, **Prisma 클라이언트를 완전히 제거하려면 13개 모두를 `db.ts``query` 함수로 교체**해야 합니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ------------------------------------------------- |
| 파일 위치 | `backend-node/src/services/dynamicFormService.ts` |
| 파일 크기 | 1,213 라인 |
| Prisma 호출 | 0개 (전환 완료) |
| **현재 진행률** | **13/13 (100%)****완료** |
| **전환 상태** | **Raw Query로 전환 완료** |
| 복잡도 | 낮음 (SQL 작성 완료 → `query()` 함수로 교체 완료) |
| 우선순위 | 🟢 낮음 (Phase 2.4) |
| **상태** | ✅ **전환 완료 및 컴파일 성공** |
### 🎯 전환 목표
- ✅ **13개 모든 Prisma 호출을 `db.ts``query()` 함수로 교체**
- 11개 `$queryRaw``query()` 함수로 교체
- 2개 ORM 메서드 → `query()` (SQL 새로 작성)
- ✅ 모든 단위 테스트 통과
- ✅ **Prisma import 완전 제거**
---
## 🔍 Prisma 사용 현황 분석
### 1. `$queryRaw` / `$queryRawUnsafe` 사용 (11개)
**현재 상태**: SQL은 이미 작성되어 있음 ✅
**전환 작업**: `prisma.$queryRaw``query()` 함수로 교체만 하면 됨
```typescript
// 기존
await prisma.$queryRaw<Array<{ column_name; data_type }>>`...`;
await prisma.$queryRawUnsafe(upsertQuery, ...values);
// 전환 후
import { query } from "../database/db";
await query<Array<{ column_name: string; data_type: string }>>(`...`);
await query(upsertQuery, values);
```
### 2. ORM 메서드 사용 (2개)
**현재 상태**: Prisma ORM 메서드 사용
**전환 작업**: SQL 작성 필요
#### 1. dynamic_form_data 조회 (1개)
```typescript
// Line 867: 폼 데이터 조회
const result = await prisma.dynamic_form_data.findUnique({
where: { id },
select: { data: true },
});
```
#### 2. screen_layouts 조회 (1개)
```typescript
// Line 1101: 화면 레이아웃 조회
const screenLayouts = await prisma.screen_layouts.findMany({
where: {
screen_id: screenId,
component_type: "widget",
},
select: {
component_id: true,
properties: true,
},
});
```
---
## 📝 전환 예시
### 예시 1: dynamic_form_data 조회 전환
**기존 Prisma 코드:**
```typescript
const result = await prisma.dynamic_form_data.findUnique({
where: { id },
select: { data: true },
});
```
**새로운 Raw Query 코드:**
```typescript
import { queryOne } from "../database/db";
const result = await queryOne<{ data: any }>(
`SELECT data FROM dynamic_form_data WHERE id = $1`,
[id]
);
```
### 예시 2: screen_layouts 조회 전환
**기존 Prisma 코드:**
```typescript
const screenLayouts = await prisma.screen_layouts.findMany({
where: {
screen_id: screenId,
component_type: "widget",
},
select: {
component_id: true,
properties: true,
},
});
```
**새로운 Raw Query 코드:**
```typescript
import { query } from "../database/db";
const screenLayouts = await query<{
component_id: string;
properties: any;
}>(
`SELECT component_id, properties
FROM screen_layouts
WHERE screen_id = $1 AND component_type = $2`,
[screenId, "widget"]
);
```
---
## 🧪 테스트 계획
### 단위 테스트 (5개)
```typescript
describe("DynamicFormService Raw Query 전환 테스트", () => {
describe("getFormDataById", () => {
test("폼 데이터 조회 성공", async () => { ... });
test("존재하지 않는 데이터", async () => { ... });
});
describe("getScreenLayoutsForControl", () => {
test("화면 레이아웃 조회 성공", async () => { ... });
test("widget 타입만 필터링", async () => { ... });
test("빈 결과 처리", async () => { ... });
});
});
```
### 통합 테스트 (3개 시나리오)
```typescript
describe("동적 폼 통합 테스트", () => {
test("폼 데이터 UPSERT → 조회", async () => { ... });
test("폼 데이터 업데이트 → 조회", async () => { ... });
test("화면 레이아웃 조회 → 제어 설정 확인", async () => { ... });
});
```
---
## 📋 전환 완료 내역
### ✅ 전환된 함수들 (13개 Raw Query 호출)
1. **getTableColumnInfo()** - 컬럼 정보 조회
2. **getPrimaryKeyColumns()** - 기본 키 조회
3. **getNotNullColumns()** - NOT NULL 컬럼 조회
4. **upsertFormData()** - UPSERT 실행
5. **partialUpdateFormData()** - 부분 업데이트
6. **updateFormData()** - 전체 업데이트
7. **deleteFormData()** - 데이터 삭제
8. **getFormDataById()** - 폼 데이터 조회
9. **getTableColumns()** - 테이블 컬럼 조회
10. **getTablePrimaryKeys()** - 기본 키 조회
11. **getScreenLayoutsForControl()** - 화면 레이아웃 조회
### 🔧 주요 기술적 해결 사항
1. **Prisma import 완전 제거**: `import { query, queryOne } from "../database/db"`
2. **동적 UPSERT 쿼리**: PostgreSQL ON CONFLICT 구문 사용
3. **부분 업데이트**: 동적 SET 절 생성
4. **타입 변환**: PostgreSQL 타입 자동 변환 로직 유지
## 📋 체크리스트
### 1단계: ORM 호출 전환 ✅ **완료**
- [x] `getFormDataById()` - queryOne 전환
- [x] `getScreenLayoutsForControl()` - query 전환
- [x] 모든 Raw Query 함수 전환
### 2단계: 테스트 & 검증 ⏳ **진행 예정**
- [ ] 단위 테스트 작성 (5개)
- [ ] 통합 테스트 작성 (3개 시나리오)
- [x] Prisma import 완전 제거 확인 ✅
- [ ] 성능 테스트
---
## 🎯 완료 기준
- [x] **13개 모든 Prisma 호출을 Raw Query로 전환 완료**
- [x] 11개 `$queryRaw``query()` 함수로 교체 ✅
- [x] 2개 ORM 메서드 → `query()` 함수로 전환 ✅
- [x] **모든 TypeScript 컴파일 오류 해결**
- [x] **`import prisma` 완전 제거** ✅
- [ ] **모든 단위 테스트 통과 (5개)**
- [ ] **모든 통합 테스트 작성 완료 (3개 시나리오)**
- [ ] **성능 저하 없음**
---
**작성일**: 2025-09-30
**완료일**: 2025-10-01
**소요 시간**: 완료됨 (이전에 전환)
**담당자**: 백엔드 개발팀
**우선순위**: 🟢 낮음 (Phase 2.4)
**상태**: ✅ **전환 완료** (테스트 필요)
**특이사항**: SQL은 이미 작성되어 있었고, `query()` 함수로 교체 완료

View File

@ -1,125 +0,0 @@
# 🔌 Phase 2.5: ExternalDbConnectionService Raw Query 전환 계획
## 📋 개요
ExternalDbConnectionService는 **15개의 Prisma 호출**이 있으며, 외부 데이터베이스 연결 정보를 관리하는 서비스입니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ---------------------------------------------------------- |
| 파일 위치 | `backend-node/src/services/externalDbConnectionService.ts` |
| 파일 크기 | 1,100+ 라인 |
| Prisma 호출 | 0개 (전환 완료) |
| **현재 진행률** | **15/15 (100%)****완료** |
| 복잡도 | 중간 (CRUD + 연결 테스트) |
| 우선순위 | 🟡 중간 (Phase 2.5) |
| **상태** | ✅ **전환 완료 및 컴파일 성공** |
### 🎯 전환 목표
- ✅ 15개 Prisma 호출을 모두 Raw Query로 전환
- ✅ 민감 정보 암호화 처리 유지
- ✅ 연결 테스트 로직 정상 동작
- ✅ 모든 단위 테스트 통과
---
## 🔍 주요 기능
### 1. 외부 DB 연결 정보 CRUD
- 생성, 조회, 수정, 삭제
- 연결 정보 암호화/복호화
### 2. 연결 테스트
- MySQL, PostgreSQL, MSSQL, Oracle 연결 테스트
### 3. 연결 정보 관리
- 회사별 연결 정보 조회
- 활성/비활성 상태 관리
---
## 📝 예상 전환 패턴
### CRUD 작업
```typescript
// 생성
await query(
`INSERT INTO external_db_connections
(connection_name, db_type, host, port, database_name, username, password, company_code)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *`,
[...]
);
// 조회
await query(
`SELECT * FROM external_db_connections
WHERE company_code = $1 AND is_active = 'Y'`,
[companyCode]
);
// 수정
await query(
`UPDATE external_db_connections
SET connection_name = $1, host = $2, ...
WHERE connection_id = $2`,
[...]
);
// 삭제 (소프트)
await query(
`UPDATE external_db_connections
SET is_active = 'N'
WHERE connection_id = $1`,
[connectionId]
);
```
---
## 📋 전환 완료 내역
### ✅ 전환된 함수들 (15개 Prisma 호출)
1. **getConnections()** - 동적 WHERE 조건 생성으로 전환
2. **getConnectionsGroupedByType()** - DB 타입 카테고리 조회
3. **getConnectionById()** - 단일 연결 조회 (비밀번호 마스킹)
4. **getConnectionByIdWithPassword()** - 비밀번호 포함 조회
5. **createConnection()** - 새 연결 생성 + 중복 확인
6. **updateConnection()** - 동적 필드 업데이트
7. **deleteConnection()** - 물리 삭제
8. **testConnectionById()** - 연결 테스트용 조회
9. **getDecryptedPassword()** - 비밀번호 복호화용 조회
10. **executeQuery()** - 쿼리 실행용 조회
11. **getTables()** - 테이블 목록 조회용
### 🔧 주요 기술적 해결 사항
1. **동적 WHERE 조건 생성**: 필터 조건에 따라 동적으로 SQL 생성
2. **동적 UPDATE 쿼리**: 변경된 필드만 업데이트하도록 구현
3. **ILIKE 검색**: 대소문자 구분 없는 검색 지원
4. **암호화 로직 유지**: PasswordEncryption 클래스와 통합 유지
## 🎯 완료 기준
- [x] **15개 Prisma 호출 모두 Raw Query로 전환**
- [x] **암호화/복호화 로직 정상 동작**
- [x] **연결 테스트 정상 동작**
- [ ] **모든 단위 테스트 통과 (10개 이상)**
- [x] **Prisma import 완전 제거**
- [x] **TypeScript 컴파일 성공**
---
**작성일**: 2025-09-30
**완료일**: 2025-10-01
**소요 시간**: 1시간
**담당자**: 백엔드 개발팀
**우선순위**: 🟡 중간 (Phase 2.5)
**상태**: ✅ **전환 완료** (테스트 필요)

View File

@ -1,225 +0,0 @@
# 🎮 Phase 2.6: DataflowControlService Raw Query 전환 계획
## 📋 개요
DataflowControlService는 **6개의 Prisma 호출**이 있으며, 데이터플로우 제어 및 실행을 담당하는 서비스입니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ----------------------------------------------------- |
| 파일 위치 | `backend-node/src/services/dataflowControlService.ts` |
| 파일 크기 | 1,100+ 라인 |
| Prisma 호출 | 0개 (전환 완료) |
| **현재 진행률** | **6/6 (100%)****완료** |
| 복잡도 | 높음 (복잡한 비즈니스 로직) |
| 우선순위 | 🟡 중간 (Phase 2.6) |
| **상태** | ✅ **전환 완료 및 컴파일 성공** |
### 🎯 전환 목표
- ✅ **6개 모든 Prisma 호출을 `db.ts``query()` 함수로 교체**
- ✅ 복잡한 비즈니스 로직 정상 동작 확인
- ✅ 모든 단위 테스트 통과
- ✅ **Prisma import 완전 제거**
---
## 🔍 Prisma 사용 현황 분석
### 주요 기능
1. **데이터플로우 실행 관리**
- 관계 기반 데이터 조회 및 저장
- 조건부 실행 로직
2. **트랜잭션 처리**
- 여러 테이블에 걸친 데이터 처리
3. **데이터 변환 및 매핑**
- 소스-타겟 데이터 변환
---
## 📝 전환 계획
### 1단계: 기본 조회 전환 (2개 함수)
**함수 목록**:
- `getRelationshipById()` - 관계 정보 조회
- `getDataflowConfig()` - 데이터플로우 설정 조회
### 2단계: 데이터 실행 로직 전환 (2개 함수)
**함수 목록**:
- `executeDataflow()` - 데이터플로우 실행
- `validateDataflow()` - 데이터플로우 검증
### 3단계: 복잡한 기능 - 트랜잭션 (2개 함수)
**함수 목록**:
- `executeWithTransaction()` - 트랜잭션 내 실행
- `rollbackOnError()` - 에러 시 롤백
---
## 💻 전환 예시
### 예시 1: 관계 정보 조회
```typescript
// 기존 Prisma
const relationship = await prisma.table_relationship.findUnique({
where: { relationship_id: relationshipId },
include: {
source_table: true,
target_table: true,
},
});
// 전환 후
import { query } from "../database/db";
const relationship = await query<TableRelationship>(
`SELECT
tr.*,
st.table_name as source_table_name,
tt.table_name as target_table_name
FROM table_relationship tr
LEFT JOIN table_labels st ON tr.source_table_id = st.table_id
LEFT JOIN table_labels tt ON tr.target_table_id = tt.table_id
WHERE tr.relationship_id = $1`,
[relationshipId]
);
```
### 예시 2: 트랜잭션 내 실행
```typescript
// 기존 Prisma
await prisma.$transaction(async (tx) => {
// 소스 데이터 조회
const sourceData = await tx.dynamic_form_data.findMany(...);
// 타겟 데이터 저장
await tx.dynamic_form_data.createMany(...);
// 실행 로그 저장
await tx.dataflow_execution_log.create(...);
});
// 전환 후
import { transaction } from "../database/db";
await transaction(async (client) => {
// 소스 데이터 조회
const sourceData = await client.query(
`SELECT * FROM dynamic_form_data WHERE ...`,
[...]
);
// 타겟 데이터 저장
await client.query(
`INSERT INTO dynamic_form_data (...) VALUES (...)`,
[...]
);
// 실행 로그 저장
await client.query(
`INSERT INTO dataflow_execution_log (...) VALUES (...)`,
[...]
);
});
```
---
## ✅ 5단계: 테스트 & 검증
### 단위 테스트 (10개)
- [ ] getRelationshipById - 관계 정보 조회
- [ ] getDataflowConfig - 설정 조회
- [ ] executeDataflow - 데이터플로우 실행
- [ ] validateDataflow - 검증
- [ ] executeWithTransaction - 트랜잭션 실행
- [ ] rollbackOnError - 에러 처리
- [ ] transformData - 데이터 변환
- [ ] mapSourceToTarget - 필드 매핑
- [ ] applyConditions - 조건 적용
- [ ] logExecution - 실행 로그
### 통합 테스트 (4개 시나리오)
1. **데이터플로우 실행 시나리오**
- 관계 조회 → 데이터 실행 → 로그 저장
2. **트랜잭션 테스트**
- 여러 테이블 동시 처리
- 에러 발생 시 롤백
3. **조건부 실행 테스트**
- 조건에 따른 데이터 처리
4. **데이터 변환 테스트**
- 소스-타겟 데이터 매핑
---
## 📋 전환 완료 내역
### ✅ 전환된 함수들 (6개 Prisma 호출)
1. **executeDataflowControl()** - 관계도 정보 조회 (findUnique → queryOne)
2. **evaluateActionConditions()** - 대상 테이블 조건 확인 ($queryRawUnsafe → query)
3. **executeInsertAction()** - INSERT 실행 ($executeRawUnsafe → query)
4. **executeUpdateAction()** - UPDATE 실행 ($executeRawUnsafe → query)
5. **executeDeleteAction()** - DELETE 실행 ($executeRawUnsafe → query)
6. **checkColumnExists()** - 컬럼 존재 확인 ($queryRawUnsafe → query)
### 🔧 주요 기술적 해결 사항
1. **Prisma import 완전 제거**: `import { query, queryOne } from "../database/db"`
2. **동적 테이블 쿼리 전환**: `$queryRawUnsafe` / `$executeRawUnsafe``query()`
3. **파라미터 바인딩 수정**: MySQL `?` → PostgreSQL `$1, $2...`
4. **복잡한 비즈니스 로직 유지**: 조건부 실행, 다중 커넥션, 에러 처리
## 🎯 완료 기준
- [x] **6개 모든 Prisma 호출을 Raw Query로 전환 완료**
- [x] **모든 TypeScript 컴파일 오류 해결**
- [x] **`import prisma` 완전 제거** ✅
- [ ] **트랜잭션 정상 동작 확인**
- [ ] **복잡한 비즈니스 로직 정상 동작**
- [ ] **모든 단위 테스트 통과 (10개)**
- [ ] **모든 통합 테스트 작성 완료 (4개 시나리오)**
- [ ] **성능 저하 없음**
---
## 💡 특이사항
### 복잡한 비즈니스 로직
이 서비스는 데이터플로우 제어라는 복잡한 비즈니스 로직을 처리합니다:
- 조건부 실행 로직
- 데이터 변환 및 매핑
- 트랜잭션 관리
- 에러 처리 및 롤백
### 성능 최적화 중요
데이터플로우 실행은 대량의 데이터를 처리할 수 있으므로:
- 배치 처리 고려
- 인덱스 활용
- 쿼리 최적화
---
**작성일**: 2025-09-30
**완료일**: 2025-10-01
**소요 시간**: 30분
**담당자**: 백엔드 개발팀
**우선순위**: 🟡 중간 (Phase 2.6)
**상태**: ✅ **전환 완료** (테스트 필요)
**특이사항**: 복잡한 비즈니스 로직이 포함되어 있어 신중한 테스트 필요

View File

@ -1,175 +0,0 @@
# 🔧 Phase 2.7: DDLExecutionService Raw Query 전환 계획
## 📋 개요
DDLExecutionService는 **4개의 Prisma 호출**이 있으며, DDL(Data Definition Language) 실행 및 관리를 담당하는 서비스입니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | -------------------------------------------------- |
| 파일 위치 | `backend-node/src/services/ddlExecutionService.ts` |
| 파일 크기 | 400+ 라인 |
| Prisma 호출 | 4개 |
| **현재 진행률** | **6/6 (100%)****완료** |
| 복잡도 | 중간 (DDL 실행 + 로그 관리) |
| 우선순위 | 🔴 최우선 (테이블 추가 기능 - Phase 2.3으로 변경) |
### 🎯 전환 목표
- ✅ **4개 모든 Prisma 호출을 `db.ts``query()` 함수로 교체**
- ✅ DDL 실행 정상 동작 확인
- ✅ 모든 단위 테스트 통과
- ✅ **Prisma import 완전 제거**
---
## 🔍 Prisma 사용 현황 분석
### 주요 기능
1. **DDL 실행**
- CREATE TABLE, ALTER TABLE, DROP TABLE
- CREATE INDEX, DROP INDEX
2. **실행 로그 관리**
- DDL 실행 이력 저장
- 에러 로그 관리
3. **롤백 지원**
- DDL 롤백 SQL 생성 및 실행
---
## 📝 전환 계획
### 1단계: DDL 실행 전환 (2개 함수)
**함수 목록**:
- `executeDDL()` - DDL 실행
- `validateDDL()` - DDL 문법 검증
### 2단계: 로그 관리 전환 (2개 함수)
**함수 목록**:
- `saveDDLLog()` - 실행 로그 저장
- `getDDLHistory()` - 실행 이력 조회
---
## 💻 전환 예시
### 예시 1: DDL 실행 및 로그 저장
```typescript
// 기존 Prisma
await prisma.$executeRawUnsafe(ddlQuery);
await prisma.ddl_execution_log.create({
data: {
ddl_statement: ddlQuery,
execution_status: "SUCCESS",
executed_by: userId,
},
});
// 전환 후
import { query } from "../database/db";
await query(ddlQuery);
await query(
`INSERT INTO ddl_execution_log
(ddl_statement, execution_status, executed_by, executed_date)
VALUES ($1, $2, $3, $4)`,
[ddlQuery, "SUCCESS", userId, new Date()]
);
```
### 예시 2: DDL 실행 이력 조회
```typescript
// 기존 Prisma
const history = await prisma.ddl_execution_log.findMany({
where: {
company_code: companyCode,
execution_status: "SUCCESS",
},
orderBy: { executed_date: "desc" },
take: 50,
});
// 전환 후
import { query } from "../database/db";
const history = await query<DDLLog[]>(
`SELECT * FROM ddl_execution_log
WHERE company_code = $1
AND execution_status = $2
ORDER BY executed_date DESC
LIMIT $3`,
[companyCode, "SUCCESS", 50]
);
```
---
## ✅ 3단계: 테스트 & 검증
### 단위 테스트 (8개)
- [ ] executeDDL - CREATE TABLE
- [ ] executeDDL - ALTER TABLE
- [ ] executeDDL - DROP TABLE
- [ ] executeDDL - CREATE INDEX
- [ ] validateDDL - 문법 검증
- [ ] saveDDLLog - 로그 저장
- [ ] getDDLHistory - 이력 조회
- [ ] rollbackDDL - DDL 롤백
### 통합 테스트 (3개 시나리오)
1. **테이블 생성 → 로그 저장 → 이력 조회**
2. **DDL 실행 실패 → 에러 로그 저장**
3. **DDL 롤백 테스트**
---
## 🎯 완료 기준
- [ ] **4개 모든 Prisma 호출을 Raw Query로 전환 완료**
- [ ] **모든 TypeScript 컴파일 오류 해결**
- [ ] **DDL 실행 정상 동작 확인**
- [ ] **모든 단위 테스트 통과 (8개)**
- [ ] **모든 통합 테스트 작성 완료 (3개 시나리오)**
- [ ] **`import prisma` 완전 제거 및 `import { query } from "../database/db"` 사용**
- [ ] **성능 저하 없음**
---
## 💡 특이사항
### DDL 실행의 위험성
DDL은 데이터베이스 스키마를 변경하므로 매우 신중하게 처리해야 합니다:
- 실행 전 검증 필수
- 롤백 SQL 자동 생성
- 실행 이력 철저히 관리
### 트랜잭션 지원 제한
PostgreSQL에서 일부 DDL은 트랜잭션을 지원하지만, 일부는 자동 커밋됩니다:
- CREATE TABLE: 트랜잭션 지원 ✅
- DROP TABLE: 트랜잭션 지원 ✅
- CREATE INDEX CONCURRENTLY: 트랜잭션 미지원 ❌
---
**작성일**: 2025-09-30
**예상 소요 시간**: 0.5일
**담당자**: 백엔드 개발팀
**우선순위**: 🟢 낮음 (Phase 2.7)
**상태**: ⏳ **진행 예정**
**특이사항**: DDL 실행의 특성상 신중한 테스트 필요

View File

@ -1,566 +0,0 @@
# 🖥️ Phase 2.1: ScreenManagementService Raw Query 전환 계획
## 📋 개요
ScreenManagementService는 **46개의 Prisma 호출**이 있는 가장 복잡한 서비스입니다. 화면 정의, 레이아웃, 메뉴 할당, 템플릿 등 다양한 기능을 포함합니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ------------------------------------------------------ |
| 파일 위치 | `backend-node/src/services/screenManagementService.ts` |
| 파일 크기 | 1,700+ 라인 |
| Prisma 호출 | 46개 |
| **현재 진행률** | **46/46 (100%)****완료** |
| 복잡도 | 매우 높음 |
| 우선순위 | 🔴 최우선 |
### 🎯 전환 현황 (2025-09-30 업데이트)
- ✅ **Stage 1 완료**: 기본 CRUD (8개 함수) - Commit: 13c1bc4, 0e8d1d4
- ✅ **Stage 2 완료**: 레이아웃 관리 (2개 함수, 4 Prisma 호출) - Commit: 67dced7
- ✅ **Stage 3 완료**: 템플릿 & 메뉴 관리 (5개 함수) - Commit: 74351e8
- ✅ **Stage 4 완료**: 복잡한 기능 (트랜잭션) - **모든 46개 Prisma 호출 전환 완료**
---
## 🔍 Prisma 사용 현황 분석
### 1. 화면 정의 관리 (Screen Definitions) - 18개
```typescript
// Line 53: 화면 코드 중복 확인
await prisma.screen_definitions.findFirst({ where: { screen_code, is_active: { not: "D" } } })
// Line 70: 화면 생성
await prisma.screen_definitions.create({ data: { ... } })
// Line 99: 화면 목록 조회 (페이징)
await prisma.screen_definitions.findMany({ where, skip, take, orderBy })
// Line 105: 화면 총 개수
await prisma.screen_definitions.count({ where })
// Line 166: 전체 화면 목록
await prisma.screen_definitions.findMany({ where })
// Line 178: 화면 코드로 조회
await prisma.screen_definitions.findFirst({ where: { screen_code } })
// Line 205: 화면 ID로 조회
await prisma.screen_definitions.findFirst({ where: { screen_id } })
// Line 221: 화면 존재 확인
await prisma.screen_definitions.findUnique({ where: { screen_id } })
// Line 236: 화면 업데이트
await prisma.screen_definitions.update({ where, data })
// Line 268: 화면 복사 - 원본 조회
await prisma.screen_definitions.findUnique({ where, include: { screen_layouts } })
// Line 292: 화면 순서 변경 - 전체 조회
await prisma.screen_definitions.findMany({ where })
// Line 486: 화면 템플릿 적용 - 존재 확인
await prisma.screen_definitions.findUnique({ where })
// Line 557: 화면 복사 - 존재 확인
await prisma.screen_definitions.findUnique({ where })
// Line 578: 화면 복사 - 중복 확인
await prisma.screen_definitions.findFirst({ where })
// Line 651: 화면 삭제 - 존재 확인
await prisma.screen_definitions.findUnique({ where })
// Line 672: 화면 삭제 (물리 삭제)
await prisma.screen_definitions.delete({ where })
// Line 700: 삭제된 화면 조회
await prisma.screen_definitions.findMany({ where: { is_active: "D" } })
// Line 706: 삭제된 화면 개수
await prisma.screen_definitions.count({ where })
// Line 763: 일괄 삭제 - 화면 조회
await prisma.screen_definitions.findMany({ where })
// Line 1083: 레이아웃 저장 - 화면 확인
await prisma.screen_definitions.findUnique({ where })
// Line 1181: 레이아웃 조회 - 화면 확인
await prisma.screen_definitions.findUnique({ where })
// Line 1655: 위젯 데이터 저장 - 화면 존재 확인
await prisma.screen_definitions.findMany({ where })
```
### 2. 레이아웃 관리 (Screen Layouts) - 4개
```typescript
// Line 1096: 레이아웃 삭제
await prisma.screen_layouts.deleteMany({ where: { screen_id } });
// Line 1107: 레이아웃 생성 (단일)
await prisma.screen_layouts.create({ data });
// Line 1152: 레이아웃 생성 (다중)
await prisma.screen_layouts.create({ data });
// Line 1193: 레이아웃 조회
await prisma.screen_layouts.findMany({ where });
```
### 3. 템플릿 관리 (Screen Templates) - 2개
```typescript
// Line 1303: 템플릿 목록 조회
await prisma.screen_templates.findMany({ where });
// Line 1317: 템플릿 생성
await prisma.screen_templates.create({ data });
```
### 4. 메뉴 할당 (Screen Menu Assignments) - 5개
```typescript
// Line 446: 메뉴 할당 조회
await prisma.screen_menu_assignments.findMany({ where });
// Line 1346: 메뉴 할당 중복 확인
await prisma.screen_menu_assignments.findFirst({ where });
// Line 1358: 메뉴 할당 생성
await prisma.screen_menu_assignments.create({ data });
// Line 1376: 화면별 메뉴 할당 조회
await prisma.screen_menu_assignments.findMany({ where });
// Line 1401: 메뉴 할당 삭제
await prisma.screen_menu_assignments.deleteMany({ where });
```
### 5. 테이블 레이블 (Table Labels) - 3개
```typescript
// Line 117: 테이블 레이블 조회 (페이징)
await prisma.table_labels.findMany({ where, skip, take });
// Line 713: 테이블 레이블 조회 (전체)
await prisma.table_labels.findMany({ where });
```
### 6. 컬럼 레이블 (Column Labels) - 2개
```typescript
// Line 948: 웹타입 정보 조회
await prisma.column_labels.findMany({ where, select });
// Line 1456: 컬럼 레이블 UPSERT
await prisma.column_labels.upsert({ where, create, update });
```
### 7. Raw Query 사용 (이미 있음) - 6개
```typescript
// Line 627: 화면 순서 변경 (일괄 업데이트)
await prisma.$executeRaw`UPDATE screen_definitions SET display_order = ...`;
// Line 833: 테이블 목록 조회
await prisma.$queryRaw<Array<{ table_name: string }>>`SELECT tablename ...`;
// Line 876: 테이블 존재 확인
await prisma.$queryRaw<Array<{ table_name: string }>>`SELECT tablename ...`;
// Line 922: 테이블 컬럼 정보 조회
await prisma.$queryRaw<Array<ColumnInfo>>`SELECT column_name, data_type ...`;
// Line 1418: 컬럼 정보 조회 (상세)
await prisma.$queryRaw`SELECT column_name, data_type ...`;
```
### 8. 트랜잭션 사용 - 3개
```typescript
// Line 521: 화면 템플릿 적용 트랜잭션
await prisma.$transaction(async (tx) => { ... })
// Line 593: 화면 복사 트랜잭션
await prisma.$transaction(async (tx) => { ... })
// Line 788: 일괄 삭제 트랜잭션
await prisma.$transaction(async (tx) => { ... })
// Line 1697: 위젯 데이터 저장 트랜잭션
await prisma.$transaction(async (tx) => { ... })
```
---
## 🛠️ 전환 전략
### 전략 1: 단계적 전환
1. **1단계**: 단순 CRUD 전환 (findFirst, findMany, create, update, delete)
2. **2단계**: 복잡한 조회 전환 (include, join)
3. **3단계**: 트랜잭션 전환
4. **4단계**: Raw Query 개선
### 전략 2: 함수별 전환 우선순위
#### 🔴 최우선 (기본 CRUD)
- `createScreen()` - Line 70
- `getScreensByCompany()` - Line 99-105
- `getScreenByCode()` - Line 178
- `getScreenById()` - Line 205
- `updateScreen()` - Line 236
- `deleteScreen()` - Line 672
#### 🟡 2순위 (레이아웃)
- `saveLayout()` - Line 1096-1152
- `getLayout()` - Line 1193
- `deleteLayout()` - Line 1096
#### 🟢 3순위 (템플릿 & 메뉴)
- `getTemplates()` - Line 1303
- `createTemplate()` - Line 1317
- `assignToMenu()` - Line 1358
- `getMenuAssignments()` - Line 1376
- `removeMenuAssignment()` - Line 1401
#### 🔵 4순위 (복잡한 기능)
- `copyScreen()` - Line 593 (트랜잭션)
- `applyTemplate()` - Line 521 (트랜잭션)
- `bulkDelete()` - Line 788 (트랜잭션)
- `reorderScreens()` - Line 627 (Raw Query)
---
## 📝 전환 예시
### 예시 1: createScreen() 전환
**기존 Prisma 코드:**
```typescript
// Line 53: 중복 확인
const existingScreen = await prisma.screen_definitions.findFirst({
where: {
screen_code: screenData.screenCode,
is_active: { not: "D" },
},
});
// Line 70: 생성
const screen = await prisma.screen_definitions.create({
data: {
screen_name: screenData.screenName,
screen_code: screenData.screenCode,
table_name: screenData.tableName,
company_code: screenData.companyCode,
description: screenData.description,
created_by: screenData.createdBy,
},
});
```
**새로운 Raw Query 코드:**
```typescript
import { query } from "../database/db";
// 중복 확인
const existingResult = await query<{ screen_id: number }>(
`SELECT screen_id FROM screen_definitions
WHERE screen_code = $1 AND is_active != 'D'
LIMIT 1`,
[screenData.screenCode]
);
if (existingResult.length > 0) {
throw new Error("이미 존재하는 화면 코드입니다.");
}
// 생성
const [screen] = await query<ScreenDefinition>(
`INSERT INTO screen_definitions (
screen_name, screen_code, table_name, company_code, description, created_by
) VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *`,
[
screenData.screenName,
screenData.screenCode,
screenData.tableName,
screenData.companyCode,
screenData.description,
screenData.createdBy,
]
);
```
### 예시 2: getScreensByCompany() 전환 (페이징)
**기존 Prisma 코드:**
```typescript
const [screens, total] = await Promise.all([
prisma.screen_definitions.findMany({
where: whereClause,
skip: (page - 1) * size,
take: size,
orderBy: { created_at: "desc" },
}),
prisma.screen_definitions.count({ where: whereClause }),
]);
```
**새로운 Raw Query 코드:**
```typescript
const offset = (page - 1) * size;
const whereSQL =
companyCode !== "*"
? "WHERE company_code = $1 AND is_active != 'D'"
: "WHERE is_active != 'D'";
const params =
companyCode !== "*" ? [companyCode, size, offset] : [size, offset];
const [screens, totalResult] = await Promise.all([
query<ScreenDefinition>(
`SELECT * FROM screen_definitions
${whereSQL}
ORDER BY created_at DESC
LIMIT $${params.length - 1} OFFSET $${params.length}`,
params
),
query<{ count: number }>(
`SELECT COUNT(*) as count FROM screen_definitions ${whereSQL}`,
companyCode !== "*" ? [companyCode] : []
),
]);
const total = totalResult[0]?.count || 0;
```
### 예시 3: 트랜잭션 전환
**기존 Prisma 코드:**
```typescript
await prisma.$transaction(async (tx) => {
const newScreen = await tx.screen_definitions.create({ data: { ... } });
await tx.screen_layouts.createMany({ data: layouts });
});
```
**새로운 Raw Query 코드:**
```typescript
import { transaction } from "../database/db";
await transaction(async (client) => {
const [newScreen] = await client.query(
`INSERT INTO screen_definitions (...) VALUES (...) RETURNING *`,
[...]
);
for (const layout of layouts) {
await client.query(
`INSERT INTO screen_layouts (...) VALUES (...)`,
[...]
);
}
});
```
---
## 🧪 테스트 계획
### 단위 테스트
```typescript
describe("ScreenManagementService Raw Query 전환 테스트", () => {
describe("createScreen", () => {
test("화면 생성 성공", async () => { ... });
test("중복 화면 코드 에러", async () => { ... });
});
describe("getScreensByCompany", () => {
test("페이징 조회 성공", async () => { ... });
test("회사별 필터링", async () => { ... });
});
describe("copyScreen", () => {
test("화면 복사 성공 (트랜잭션)", async () => { ... });
test("레이아웃 함께 복사", async () => { ... });
});
});
```
### 통합 테스트
```typescript
describe("화면 관리 통합 테스트", () => {
test("화면 생성 → 조회 → 수정 → 삭제", async () => { ... });
test("화면 복사 → 레이아웃 확인", async () => { ... });
test("메뉴 할당 → 조회 → 해제", async () => { ... });
});
```
---
## 📋 체크리스트
### 1단계: 기본 CRUD (8개 함수) ✅ **완료**
- [x] `createScreen()` - 화면 생성
- [x] `getScreensByCompany()` - 화면 목록 (페이징)
- [x] `getScreenByCode()` - 화면 코드로 조회
- [x] `getScreenById()` - 화면 ID로 조회
- [x] `updateScreen()` - 화면 업데이트
- [x] `deleteScreen()` - 화면 삭제
- [x] `getScreens()` - 전체 화면 목록 조회
- [x] `getScreen()` - 회사 코드 필터링 포함 조회
### 2단계: 레이아웃 관리 (2개 함수) ✅ **완료**
- [x] `saveLayout()` - 레이아웃 저장 (메타데이터 + 컴포넌트)
- [x] `getLayout()` - 레이아웃 조회
- [x] 레이아웃 삭제 로직 (saveLayout 내부에 포함)
### 3단계: 템플릿 & 메뉴 (5개 함수) ✅ **완료**
- [x] `getTemplatesByCompany()` - 템플릿 목록
- [x] `createTemplate()` - 템플릿 생성
- [x] `assignScreenToMenu()` - 메뉴 할당
- [x] `getScreensByMenu()` - 메뉴별 화면 조회
- [x] `unassignScreenFromMenu()` - 메뉴 할당 해제
- [ ] 테이블 레이블 조회 (getScreensByCompany 내부에 포함됨)
### 4단계: 복잡한 기능 (4개 함수) ✅ **완료**
- [x] `copyScreen()` - 화면 복사 (트랜잭션)
- [x] `generateScreenCode()` - 화면 코드 자동 생성
- [x] `checkScreenDependencies()` - 화면 의존성 체크 (메뉴 할당 포함)
- [x] 모든 유틸리티 메서드 Raw Query 전환
### 5단계: 테스트 & 검증 ✅ **완료**
- [x] 단위 테스트 작성 (18개 테스트 통과)
- createScreen, updateScreen, deleteScreen
- getScreensByCompany, getScreenById
- saveLayout, getLayout
- getTemplatesByCompany, assignScreenToMenu
- copyScreen, generateScreenCode
- getTableColumns
- [x] 통합 테스트 작성 (6개 시나리오)
- 화면 생명주기 테스트 (생성 → 조회 → 수정 → 삭제 → 복원 → 영구삭제)
- 화면 복사 및 레이아웃 테스트
- 테이블 정보 조회 테스트
- 일괄 작업 테스트
- 화면 코드 자동 생성 테스트
- [x] Prisma import 완전 제거 확인
- [ ] 성능 테스트 (추후 실행 예정)
---
## 🎯 완료 기준
- ✅ **46개 Prisma 호출 모두 Raw Query로 전환 완료**
- ✅ **모든 TypeScript 컴파일 오류 해결**
- ✅ **트랜잭션 정상 동작 확인**
- ✅ **에러 처리 및 롤백 정상 동작**
- ✅ **모든 단위 테스트 통과 (18개)**
- ✅ **모든 통합 테스트 작성 완료 (6개 시나리오)**
- ✅ **Prisma import 완전 제거**
- [ ] 성능 저하 없음 (기존 대비 ±10% 이내) - 추후 측정 예정
## 📊 테스트 결과
### 단위 테스트 (18개)
```
✅ createScreen - 화면 생성 (2개 테스트)
✅ getScreensByCompany - 화면 목록 페이징 (2개 테스트)
✅ updateScreen - 화면 업데이트 (2개 테스트)
✅ deleteScreen - 화면 삭제 (2개 테스트)
✅ saveLayout - 레이아웃 저장 (2개 테스트)
- 기본 저장, 소수점 좌표 반올림 처리
✅ getLayout - 레이아웃 조회 (1개 테스트)
✅ getTemplatesByCompany - 템플릿 목록 (1개 테스트)
✅ assignScreenToMenu - 메뉴 할당 (2개 테스트)
✅ copyScreen - 화면 복사 (1개 테스트)
✅ generateScreenCode - 화면 코드 자동 생성 (2개 테스트)
✅ getTableColumns - 테이블 컬럼 정보 (1개 테스트)
Test Suites: 1 passed
Tests: 18 passed
Time: 1.922s
```
### 통합 테스트 (6개 시나리오)
```
✅ 화면 생명주기 테스트
- 생성 → 조회 → 수정 → 삭제 → 복원 → 영구삭제
✅ 화면 복사 및 레이아웃 테스트
- 화면 복사 → 레이아웃 저장 → 레이아웃 확인 → 레이아웃 수정
✅ 테이블 정보 조회 테스트
- 테이블 목록 조회 → 특정 테이블 정보 조회
✅ 일괄 작업 테스트
- 여러 화면 생성 → 일괄 삭제
✅ 화면 코드 자동 생성 테스트
- 순차적 화면 코드 생성 검증
✅ 메뉴 할당 테스트 (skip - 실제 메뉴 데이터 필요)
```
---
## 🐛 버그 수정 및 개선사항
### 실제 운영 환경에서 발견된 이슈
#### 1. 소수점 좌표 저장 오류 (해결 완료)
**문제**:
```
invalid input syntax for type integer: "1602.666666666667"
```
- `position_x`, `position_y`, `width`, `height` 컬럼이 `integer` 타입
- 격자 계산 시 소수점 값이 발생하여 저장 실패
**해결**:
```typescript
Math.round(component.position.x), // 정수로 반올림
Math.round(component.position.y),
Math.round(component.size.width),
Math.round(component.size.height),
```
**테스트 추가**:
- 소수점 좌표 저장 테스트 케이스 추가
- 반올림 처리 검증
**영향 범위**:
- `saveLayout()` 함수
- `copyScreen()` 함수 (레이아웃 복사 시)
---
**작성일**: 2025-09-30
**완료일**: 2025-09-30
**예상 소요 시간**: 2-3일 → **실제 소요 시간**: 1일
**담당자**: 백엔드 개발팀
**우선순위**: 🔴 최우선 (Phase 2.1)
**상태**: ✅ **완료**

View File

@ -1,407 +0,0 @@
# 📋 Phase 3.11: DDLAuditLogger Raw Query 전환 계획
## 📋 개요
DDLAuditLogger는 **8개의 Prisma 호출**이 있으며, DDL 실행 감사 로그 관리를 담당하는 서비스입니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | --------------------------------------------- |
| 파일 위치 | `backend-node/src/services/ddlAuditLogger.ts` |
| 파일 크기 | 350 라인 |
| Prisma 호출 | 0개 (전환 완료) |
| **현재 진행률** | **8/8 (100%)****전환 완료** |
| 복잡도 | 중간 (통계 쿼리, $executeRaw) |
| 우선순위 | 🟡 중간 (Phase 3.11) |
| **상태** | ✅ **완료** |
### 🎯 전환 목표
- ⏳ **8개 모든 Prisma 호출을 `db.ts``query()`, `queryOne()` 함수로 교체**
- ⏳ DDL 감사 로그 기능 정상 동작
- ⏳ 통계 쿼리 전환 (GROUP BY, COUNT, ORDER BY)
- ⏳ $executeRaw → query 전환
- ⏳ $queryRawUnsafe → query 전환
- ⏳ 동적 WHERE 조건 생성
- ⏳ TypeScript 컴파일 성공
- ⏳ **Prisma import 완전 제거**
---
## 🔍 Prisma 사용 현황 분석
### 주요 Prisma 호출 (8개)
#### 1. **logDDLStart()** - DDL 시작 로그 (INSERT)
```typescript
// Line 27
const logEntry = await prisma.$executeRaw`
INSERT INTO ddl_audit_logs (
execution_id, ddl_type, table_name, status,
executed_by, company_code, started_at, metadata
) VALUES (
${executionId}, ${ddlType}, ${tableName}, 'in_progress',
${executedBy}, ${companyCode}, NOW(), ${JSON.stringify(metadata)}::jsonb
)
`;
```
#### 2. **getAuditLogs()** - 감사 로그 목록 조회 (SELECT with filters)
```typescript
// Line 162
const logs = await prisma.$queryRawUnsafe(query, ...params);
```
- 동적 WHERE 조건 생성
- 페이징 (OFFSET, LIMIT)
- 정렬 (ORDER BY)
#### 3. **getAuditStats()** - 통계 조회 (복합 쿼리)
```typescript
// Line 199 - 총 통계
const totalStats = (await prisma.$queryRawUnsafe(
`SELECT
COUNT(*) as total_executions,
COUNT(CASE WHEN status = 'success' THEN 1 END) as successful,
COUNT(CASE WHEN status = 'failed' THEN 1 END) as failed,
AVG(EXTRACT(EPOCH FROM (completed_at - started_at))) as avg_duration
FROM ddl_audit_logs
WHERE ${whereClause}`
)) as any[];
// Line 212 - DDL 타입별 통계
const ddlTypeStats = (await prisma.$queryRawUnsafe(
`SELECT ddl_type, COUNT(*) as count
FROM ddl_audit_logs
WHERE ${whereClause}
GROUP BY ddl_type
ORDER BY count DESC`
)) as any[];
// Line 224 - 사용자별 통계
const userStats = (await prisma.$queryRawUnsafe(
`SELECT executed_by, COUNT(*) as count
FROM ddl_audit_logs
WHERE ${whereClause}
GROUP BY executed_by
ORDER BY count DESC
LIMIT 10`
)) as any[];
// Line 237 - 최근 실패 로그
const recentFailures = (await prisma.$queryRawUnsafe(
`SELECT * FROM ddl_audit_logs
WHERE status = 'failed' AND ${whereClause}
ORDER BY started_at DESC
LIMIT 5`
)) as any[];
```
#### 4. **getExecutionHistory()** - 실행 이력 조회
```typescript
// Line 287
const history = await prisma.$queryRawUnsafe(
`SELECT * FROM ddl_audit_logs
WHERE table_name = $1 AND company_code = $2
ORDER BY started_at DESC
LIMIT $3`,
tableName,
companyCode,
limit
);
```
#### 5. **cleanupOldLogs()** - 오래된 로그 삭제
```typescript
// Line 320
const result = await prisma.$executeRaw`
DELETE FROM ddl_audit_logs
WHERE started_at < NOW() - INTERVAL '${retentionDays} days'
AND company_code = ${companyCode}
`;
```
---
## 💡 전환 전략
### 1단계: $executeRaw 전환 (2개)
- `logDDLStart()` - INSERT
- `cleanupOldLogs()` - DELETE
### 2단계: 단순 $queryRawUnsafe 전환 (1개)
- `getExecutionHistory()` - 파라미터 바인딩 있음
### 3단계: 복잡한 $queryRawUnsafe 전환 (1개)
- `getAuditLogs()` - 동적 WHERE 조건
### 4단계: 통계 쿼리 전환 (4개)
- `getAuditStats()` 내부의 4개 쿼리
- GROUP BY, CASE WHEN, AVG, EXTRACT
---
## 💻 전환 예시
### 예시 1: $executeRaw → query (INSERT)
**변경 전**:
```typescript
const logEntry = await prisma.$executeRaw`
INSERT INTO ddl_audit_logs (
execution_id, ddl_type, table_name, status,
executed_by, company_code, started_at, metadata
) VALUES (
${executionId}, ${ddlType}, ${tableName}, 'in_progress',
${executedBy}, ${companyCode}, NOW(), ${JSON.stringify(metadata)}::jsonb
)
`;
```
**변경 후**:
```typescript
await query(
`INSERT INTO ddl_audit_logs (
execution_id, ddl_type, table_name, status,
executed_by, company_code, started_at, metadata
) VALUES ($1, $2, $3, $4, $5, $6, NOW(), $7::jsonb)`,
[
executionId,
ddlType,
tableName,
"in_progress",
executedBy,
companyCode,
JSON.stringify(metadata),
]
);
```
### 예시 2: 동적 WHERE 조건
**변경 전**:
```typescript
let query = `SELECT * FROM ddl_audit_logs WHERE 1=1`;
const params: any[] = [];
if (filters.ddlType) {
query += ` AND ddl_type = ?`;
params.push(filters.ddlType);
}
const logs = await prisma.$queryRawUnsafe(query, ...params);
```
**변경 후**:
```typescript
const conditions: string[] = [];
const params: any[] = [];
let paramIndex = 1;
if (filters.ddlType) {
conditions.push(`ddl_type = $${paramIndex++}`);
params.push(filters.ddlType);
}
const whereClause =
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const sql = `SELECT * FROM ddl_audit_logs ${whereClause}`;
const logs = await query<any>(sql, params);
```
### 예시 3: 통계 쿼리 (GROUP BY)
**변경 전**:
```typescript
const ddlTypeStats = (await prisma.$queryRawUnsafe(
`SELECT ddl_type, COUNT(*) as count
FROM ddl_audit_logs
WHERE ${whereClause}
GROUP BY ddl_type
ORDER BY count DESC`
)) as any[];
```
**변경 후**:
```typescript
const ddlTypeStats = await query<{ ddl_type: string; count: string }>(
`SELECT ddl_type, COUNT(*) as count
FROM ddl_audit_logs
WHERE ${whereClause}
GROUP BY ddl_type
ORDER BY count DESC`,
params
);
```
---
## 🔧 기술적 고려사항
### 1. JSON 필드 처리
`metadata` 필드는 JSONB 타입으로, INSERT 시 `::jsonb` 캐스팅 필요:
```typescript
JSON.stringify(metadata) + "::jsonb";
```
### 2. 날짜/시간 함수
- `NOW()` - 현재 시간
- `INTERVAL '30 days'` - 날짜 간격
- `EXTRACT(EPOCH FROM ...)` - 초 단위 변환
### 3. CASE WHEN 집계
```sql
COUNT(CASE WHEN status = 'success' THEN 1 END) as successful
```
### 4. 동적 WHERE 조건
여러 필터를 조합하여 WHERE 절 생성:
- ddlType
- tableName
- status
- executedBy
- dateRange (startDate, endDate)
---
## ✅ 전환 완료 내역
### 전환된 Prisma 호출 (8개)
1. **`logDDLExecution()`** - DDL 실행 로그 INSERT
- Before: `prisma.$executeRaw`
- After: `query()` with 7 parameters
2. **`getAuditLogs()`** - 감사 로그 목록 조회
- Before: `prisma.$queryRawUnsafe`
- After: `query<any>()` with dynamic WHERE clause
3. **`getDDLStatistics()`** - 통계 조회 (4개 쿼리)
- Before: 4x `prisma.$queryRawUnsafe`
- After: 4x `query<any>()`
- totalStats: 전체 실행 통계 (CASE WHEN 집계)
- ddlTypeStats: DDL 타입별 통계 (GROUP BY)
- userStats: 사용자별 통계 (GROUP BY, LIMIT 10)
- recentFailures: 최근 실패 로그 (WHERE success = false)
4. **`getTableDDLHistory()`** - 테이블별 DDL 히스토리
- Before: `prisma.$queryRawUnsafe`
- After: `query<any>()` with table_name filter
5. **`cleanupOldLogs()`** - 오래된 로그 삭제
- Before: `prisma.$executeRaw`
- After: `query()` with date filter
### 주요 기술적 개선사항
1. **파라미터 바인딩**: PostgreSQL `$1, $2, ...` 스타일로 통일
2. **동적 WHERE 조건**: 파라미터 인덱스 자동 증가 로직 유지
3. **통계 쿼리**: CASE WHEN, GROUP BY, SUM 등 복잡한 집계 쿼리 완벽 전환
4. **에러 처리**: 기존 try-catch 구조 유지
5. **로깅**: logger 유틸리티 활용 유지
### 코드 정리
- [x] `import { PrismaClient }` 제거
- [x] `const prisma = new PrismaClient()` 제거
- [x] `import { query, queryOne }` 추가
- [x] 모든 타입 정의 유지
- [x] TypeScript 컴파일 성공
- [x] Linter 오류 없음
## 📝 원본 전환 체크리스트
### 1단계: Prisma 호출 전환 (✅ 완료)
- [ ] `logDDLStart()` - INSERT ($executeRaw → query)
- [ ] `logDDLComplete()` - UPDATE (이미 query 사용 중일 가능성)
- [ ] `logDDLError()` - UPDATE (이미 query 사용 중일 가능성)
- [ ] `getAuditLogs()` - SELECT with filters ($queryRawUnsafe → query)
- [ ] `getAuditStats()` 내 4개 쿼리:
- [ ] totalStats (집계 쿼리)
- [ ] ddlTypeStats (GROUP BY)
- [ ] userStats (GROUP BY + LIMIT)
- [ ] recentFailures (필터 + ORDER BY + LIMIT)
- [ ] `getExecutionHistory()` - SELECT with params ($queryRawUnsafe → query)
- [ ] `cleanupOldLogs()` - DELETE ($executeRaw → query)
### 2단계: 코드 정리
- [ ] import 문 수정 (`prisma` → `query, queryOne`)
- [ ] Prisma import 완전 제거
- [ ] 타입 정의 확인
### 3단계: 테스트
- [ ] 단위 테스트 작성 (8개)
- [ ] DDL 시작 로그 테스트
- [ ] DDL 완료 로그 테스트
- [ ] 감사 로그 목록 조회 테스트
- [ ] 통계 조회 테스트
- [ ] 실행 이력 조회 테스트
- [ ] 오래된 로그 삭제 테스트
- [ ] 통합 테스트 작성 (3개)
- [ ] 전체 DDL 실행 플로우 테스트
- [ ] 필터링 및 페이징 테스트
- [ ] 통계 정확성 테스트
- [ ] 성능 테스트
- [ ] 대량 로그 조회 성능
- [ ] 통계 쿼리 성능
### 4단계: 문서화
- [ ] 전환 완료 문서 업데이트
- [ ] 주요 변경사항 기록
- [ ] 성능 벤치마크 결과
---
## 🎯 예상 난이도 및 소요 시간
- **난이도**: ⭐⭐⭐ (중간)
- 복잡한 통계 쿼리 (GROUP BY, CASE WHEN)
- 동적 WHERE 조건 생성
- JSON 필드 처리
- **예상 소요 시간**: 1~1.5시간
- Prisma 호출 전환: 30분
- 테스트: 20분
- 문서화: 10분
---
## 📌 참고사항
### 관련 서비스
- `DDLExecutionService` - DDL 실행 (이미 전환 완료)
- `DDLSafetyValidator` - DDL 안전성 검증
### 의존성
- `../database/db` - query, queryOne 함수
- `../types/ddl` - DDL 관련 타입
- `../utils/logger` - 로깅
---
**상태**: ⏳ **대기 중**
**특이사항**: 통계 쿼리, JSON 필드, 동적 WHERE 조건 포함

View File

@ -1,356 +0,0 @@
# 📋 Phase 3.12: ExternalCallConfigService Raw Query 전환 계획
## 📋 개요
ExternalCallConfigService는 **8개의 Prisma 호출**이 있으며, 외부 API 호출 설정 관리를 담당하는 서비스입니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | -------------------------------------------------------- |
| 파일 위치 | `backend-node/src/services/externalCallConfigService.ts` |
| 파일 크기 | 612 라인 |
| Prisma 호출 | 0개 (전환 완료) |
| **현재 진행률** | **8/8 (100%)****전환 완료** |
| 복잡도 | 중간 (JSON 필드, 복잡한 CRUD) |
| 우선순위 | 🟡 중간 (Phase 3.12) |
| **상태** | ✅ **완료** |
### 🎯 전환 목표
- ⏳ **8개 모든 Prisma 호출을 `db.ts``query()`, `queryOne()` 함수로 교체**
- ⏳ 외부 호출 설정 CRUD 기능 정상 동작
- ⏳ JSON 필드 처리 (headers, params, auth_config)
- ⏳ 동적 WHERE 조건 생성
- ⏳ 민감 정보 암호화/복호화 유지
- ⏳ TypeScript 컴파일 성공
- ⏳ **Prisma import 완전 제거**
---
## 🔍 예상 Prisma 사용 패턴
### 주요 기능 (8개 예상)
#### 1. **외부 호출 설정 목록 조회**
- findMany with filters
- 페이징, 정렬
- 동적 WHERE 조건 (is_active, company_code, search)
#### 2. **외부 호출 설정 단건 조회**
- findUnique or findFirst
- config_id 기준
#### 3. **외부 호출 설정 생성**
- create
- JSON 필드 처리 (headers, params, auth_config)
- 민감 정보 암호화
#### 4. **외부 호출 설정 수정**
- update
- 동적 UPDATE 쿼리
- JSON 필드 업데이트
#### 5. **외부 호출 설정 삭제**
- delete or soft delete
#### 6. **외부 호출 설정 복제**
- findUnique + create
#### 7. **외부 호출 설정 테스트**
- findUnique
- 실제 HTTP 호출
#### 8. **외부 호출 이력 조회**
- findMany with 관계 조인
- 통계 쿼리
---
## 💡 전환 전략
### 1단계: 기본 CRUD 전환 (5개)
- getExternalCallConfigs() - 목록 조회
- getExternalCallConfig() - 단건 조회
- createExternalCallConfig() - 생성
- updateExternalCallConfig() - 수정
- deleteExternalCallConfig() - 삭제
### 2단계: 추가 기능 전환 (3개)
- duplicateExternalCallConfig() - 복제
- testExternalCallConfig() - 테스트
- getExternalCallHistory() - 이력 조회
---
## 💻 전환 예시
### 예시 1: 목록 조회 (동적 WHERE + JSON)
**변경 전**:
```typescript
const configs = await prisma.external_call_configs.findMany({
where: {
company_code: companyCode,
is_active: isActive,
OR: [
{ config_name: { contains: search, mode: "insensitive" } },
{ endpoint_url: { contains: search, mode: "insensitive" } },
],
},
orderBy: { created_at: "desc" },
skip,
take: limit,
});
```
**변경 후**:
```typescript
const conditions: string[] = ["company_code = $1"];
const params: any[] = [companyCode];
let paramIndex = 2;
if (isActive !== undefined) {
conditions.push(`is_active = $${paramIndex++}`);
params.push(isActive);
}
if (search) {
conditions.push(
`(config_name ILIKE $${paramIndex} OR endpoint_url ILIKE $${paramIndex})`
);
params.push(`%${search}%`);
paramIndex++;
}
const configs = await query<any>(
`SELECT * FROM external_call_configs
WHERE ${conditions.join(" AND ")}
ORDER BY created_at DESC
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
[...params, limit, skip]
);
```
### 예시 2: JSON 필드 생성
**변경 전**:
```typescript
const config = await prisma.external_call_configs.create({
data: {
config_name: data.config_name,
endpoint_url: data.endpoint_url,
http_method: data.http_method,
headers: data.headers, // JSON
params: data.params, // JSON
auth_config: encryptedAuthConfig, // JSON (암호화됨)
company_code: companyCode,
},
});
```
**변경 후**:
```typescript
const config = await queryOne<any>(
`INSERT INTO external_call_configs
(config_name, endpoint_url, http_method, headers, params,
auth_config, company_code, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
RETURNING *`,
[
data.config_name,
data.endpoint_url,
data.http_method,
JSON.stringify(data.headers),
JSON.stringify(data.params),
JSON.stringify(encryptedAuthConfig),
companyCode,
]
);
```
### 예시 3: 동적 UPDATE (JSON 포함)
**변경 전**:
```typescript
const updateData: any = {};
if (data.headers) updateData.headers = data.headers;
if (data.params) updateData.params = data.params;
const config = await prisma.external_call_configs.update({
where: { config_id: configId },
data: updateData,
});
```
**변경 후**:
```typescript
const updateFields: string[] = ["updated_at = NOW()"];
const values: any[] = [];
let paramIndex = 1;
if (data.headers !== undefined) {
updateFields.push(`headers = $${paramIndex++}`);
values.push(JSON.stringify(data.headers));
}
if (data.params !== undefined) {
updateFields.push(`params = $${paramIndex++}`);
values.push(JSON.stringify(data.params));
}
const config = await queryOne<any>(
`UPDATE external_call_configs
SET ${updateFields.join(", ")}
WHERE config_id = $${paramIndex}
RETURNING *`,
[...values, configId]
);
```
---
## 🔧 기술적 고려사항
### 1. JSON 필드 처리
3개의 JSON 필드가 있을 것으로 예상:
- `headers` - HTTP 헤더
- `params` - 쿼리 파라미터
- `auth_config` - 인증 설정 (암호화됨)
```typescript
// INSERT/UPDATE 시
JSON.stringify(jsonData);
// SELECT 후
const parsedData =
typeof row.headers === "string" ? JSON.parse(row.headers) : row.headers;
```
### 2. 민감 정보 암호화
auth_config는 암호화되어 저장되므로, 기존 암호화/복호화 로직 유지:
```typescript
import { encrypt, decrypt } from "../utils/encryption";
// 저장 시
const encryptedAuthConfig = encrypt(JSON.stringify(authConfig));
// 조회 시
const decryptedAuthConfig = JSON.parse(decrypt(row.auth_config));
```
### 3. HTTP 메소드 검증
```typescript
const VALID_HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
if (!VALID_HTTP_METHODS.includes(httpMethod)) {
throw new Error("Invalid HTTP method");
}
```
### 4. URL 검증
```typescript
try {
new URL(endpointUrl);
} catch {
throw new Error("Invalid endpoint URL");
}
```
---
## ✅ 전환 완료 내역
### 전환된 Prisma 호출 (8개)
1. **`getConfigs()`** - 목록 조회 (findMany → query)
2. **`getConfigById()`** - 단건 조회 (findUnique → queryOne)
3. **`createConfig()`** - 중복 검사 (findFirst → queryOne)
4. **`createConfig()`** - 생성 (create → queryOne with INSERT)
5. **`updateConfig()`** - 중복 검사 (findFirst → queryOne)
6. **`updateConfig()`** - 수정 (update → queryOne with 동적 UPDATE)
7. **`deleteConfig()`** - 삭제 (update → query)
8. **`getExternalCallConfigsForButtonControl()`** - 조회 (findMany → query)
### 주요 기술적 개선사항
- 동적 WHERE 조건 생성 (company_code, call_type, api_type, is_active, search)
- ILIKE를 활용한 대소문자 구분 없는 검색
- 동적 UPDATE 쿼리 (9개 필드)
- JSON 필드 처리 (`config_data` → `JSON.stringify()`)
- 중복 검사 로직 유지
### 코드 정리
- [x] import 문 수정 완료
- [x] Prisma import 완전 제거
- [x] TypeScript 컴파일 성공
- [x] Linter 오류 없음
## 📝 원본 전환 체크리스트
### 1단계: Prisma 호출 전환 (✅ 완료)
- [ ] `getExternalCallConfigs()` - 목록 조회 (findMany + count)
- [ ] `getExternalCallConfig()` - 단건 조회 (findUnique)
- [ ] `createExternalCallConfig()` - 생성 (create)
- [ ] `updateExternalCallConfig()` - 수정 (update)
- [ ] `deleteExternalCallConfig()` - 삭제 (delete)
- [ ] `duplicateExternalCallConfig()` - 복제 (findUnique + create)
- [ ] `testExternalCallConfig()` - 테스트 (findUnique)
- [ ] `getExternalCallHistory()` - 이력 조회 (findMany)
### 2단계: 코드 정리
- [ ] import 문 수정 (`prisma` → `query, queryOne`)
- [ ] JSON 필드 처리 확인
- [ ] 암호화/복호화 로직 유지
- [ ] Prisma import 완전 제거
### 3단계: 테스트
- [ ] 단위 테스트 작성 (8개)
- [ ] 통합 테스트 작성 (3개)
- [ ] 암호화 테스트
- [ ] HTTP 호출 테스트
### 4단계: 문서화
- [ ] 전환 완료 문서 업데이트
- [ ] API 문서 업데이트
---
## 🎯 예상 난이도 및 소요 시간
- **난이도**: ⭐⭐⭐ (중간)
- JSON 필드 처리
- 암호화/복호화 로직
- HTTP 호출 테스트
- **예상 소요 시간**: 1~1.5시간
---
**상태**: ⏳ **대기 중**
**특이사항**: JSON 필드, 민감 정보 암호화, HTTP 호출 포함

View File

@ -1,338 +0,0 @@
# 📋 Phase 3.13: EntityJoinService Raw Query 전환 계획
## 📋 개요
EntityJoinService는 **5개의 Prisma 호출**이 있으며, 엔티티 간 조인 관계 관리를 담당하는 서비스입니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ------------------------------------------------ |
| 파일 위치 | `backend-node/src/services/entityJoinService.ts` |
| 파일 크기 | 575 라인 |
| Prisma 호출 | 0개 (전환 완료) |
| **현재 진행률** | **5/5 (100%)****전환 완료** |
| 복잡도 | 중간 (조인 쿼리, 관계 설정) |
| 우선순위 | 🟡 중간 (Phase 3.13) |
| **상태** | ✅ **완료** |
### 🎯 전환 목표
- ⏳ **5개 모든 Prisma 호출을 `db.ts``query()`, `queryOne()` 함수로 교체**
- ⏳ 엔티티 조인 설정 CRUD 기능 정상 동작
- ⏳ 복잡한 조인 쿼리 전환 (LEFT JOIN, INNER JOIN)
- ⏳ 조인 유효성 검증
- ⏳ TypeScript 컴파일 성공
- ⏳ **Prisma import 완전 제거**
---
## 🔍 예상 Prisma 사용 패턴
### 주요 기능 (5개 예상)
#### 1. **엔티티 조인 목록 조회**
- findMany with filters
- 동적 WHERE 조건
- 페이징, 정렬
#### 2. **엔티티 조인 단건 조회**
- findUnique or findFirst
- join_id 기준
#### 3. **엔티티 조인 생성**
- create
- 조인 유효성 검증
#### 4. **엔티티 조인 수정**
- update
- 동적 UPDATE 쿼리
#### 5. **엔티티 조인 삭제**
- delete
---
## 💡 전환 전략
### 1단계: 기본 CRUD 전환 (5개)
- getEntityJoins() - 목록 조회
- getEntityJoin() - 단건 조회
- createEntityJoin() - 생성
- updateEntityJoin() - 수정
- deleteEntityJoin() - 삭제
---
## 💻 전환 예시
### 예시 1: 조인 설정 조회 (LEFT JOIN으로 테이블 정보 포함)
**변경 전**:
```typescript
const joins = await prisma.entity_joins.findMany({
where: {
company_code: companyCode,
is_active: true,
},
include: {
source_table: true,
target_table: true,
},
orderBy: { created_at: "desc" },
});
```
**변경 후**:
```typescript
const joins = await query<any>(
`SELECT
ej.*,
st.table_name as source_table_name,
st.table_label as source_table_label,
tt.table_name as target_table_name,
tt.table_label as target_table_label
FROM entity_joins ej
LEFT JOIN tables st ON ej.source_table_id = st.table_id
LEFT JOIN tables tt ON ej.target_table_id = tt.table_id
WHERE ej.company_code = $1 AND ej.is_active = $2
ORDER BY ej.created_at DESC`,
[companyCode, true]
);
```
### 예시 2: 조인 생성 (유효성 검증 포함)
**변경 전**:
```typescript
// 조인 유효성 검증
const sourceTable = await prisma.tables.findUnique({
where: { table_id: sourceTableId },
});
const targetTable = await prisma.tables.findUnique({
where: { table_id: targetTableId },
});
if (!sourceTable || !targetTable) {
throw new Error("Invalid table references");
}
// 조인 생성
const join = await prisma.entity_joins.create({
data: {
source_table_id: sourceTableId,
target_table_id: targetTableId,
join_type: joinType,
join_condition: joinCondition,
company_code: companyCode,
},
});
```
**변경 후**:
```typescript
// 조인 유효성 검증 (Promise.all로 병렬 실행)
const [sourceTable, targetTable] = await Promise.all([
queryOne<any>(`SELECT * FROM tables WHERE table_id = $1`, [sourceTableId]),
queryOne<any>(`SELECT * FROM tables WHERE table_id = $1`, [targetTableId]),
]);
if (!sourceTable || !targetTable) {
throw new Error("Invalid table references");
}
// 조인 생성
const join = await queryOne<any>(
`INSERT INTO entity_joins
(source_table_id, target_table_id, join_type, join_condition,
company_code, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
RETURNING *`,
[sourceTableId, targetTableId, joinType, joinCondition, companyCode]
);
```
### 예시 3: 조인 수정
**변경 전**:
```typescript
const join = await prisma.entity_joins.update({
where: { join_id: joinId },
data: {
join_type: joinType,
join_condition: joinCondition,
is_active: isActive,
},
});
```
**변경 후**:
```typescript
const updateFields: string[] = ["updated_at = NOW()"];
const values: any[] = [];
let paramIndex = 1;
if (joinType !== undefined) {
updateFields.push(`join_type = $${paramIndex++}`);
values.push(joinType);
}
if (joinCondition !== undefined) {
updateFields.push(`join_condition = $${paramIndex++}`);
values.push(joinCondition);
}
if (isActive !== undefined) {
updateFields.push(`is_active = $${paramIndex++}`);
values.push(isActive);
}
const join = await queryOne<any>(
`UPDATE entity_joins
SET ${updateFields.join(", ")}
WHERE join_id = $${paramIndex}
RETURNING *`,
[...values, joinId]
);
```
---
## 🔧 기술적 고려사항
### 1. 조인 타입 검증
```typescript
const VALID_JOIN_TYPES = ["INNER", "LEFT", "RIGHT", "FULL"];
if (!VALID_JOIN_TYPES.includes(joinType)) {
throw new Error("Invalid join type");
}
```
### 2. 조인 조건 검증
```typescript
// 조인 조건은 SQL 조건식 형태 (예: "source.id = target.parent_id")
// SQL 인젝션 방지를 위한 검증 필요
const isValidJoinCondition = /^[\w\s.=<>]+$/.test(joinCondition);
if (!isValidJoinCondition) {
throw new Error("Invalid join condition");
}
```
### 3. 순환 참조 방지
```typescript
// 조인이 순환 참조를 만들지 않는지 검증
async function checkCircularReference(
sourceTableId: number,
targetTableId: number
): Promise<boolean> {
// 재귀적으로 조인 관계 확인
// ...
}
```
### 4. LEFT JOIN으로 관련 테이블 정보 조회
조인 설정 조회 시 source/target 테이블 정보를 함께 가져오기 위해 LEFT JOIN 사용
---
## ✅ 전환 완료 내역
### 전환된 Prisma 호출 (5개)
1. **`detectEntityJoins()`** - 엔티티 컬럼 감지 (findMany → query)
- column_labels 조회
- web_type = 'entity' 필터
- reference_table/reference_column IS NOT NULL
2. **`validateJoinConfig()`** - 테이블 존재 확인 ($queryRaw → query)
- information_schema.tables 조회
- 참조 테이블 검증
3. **`validateJoinConfig()`** - 컬럼 존재 확인 ($queryRaw → query)
- information_schema.columns 조회
- 표시 컬럼 검증
4. **`getReferenceTableColumns()`** - 컬럼 정보 조회 ($queryRaw → query)
- information_schema.columns 조회
- 문자열 타입 컬럼만 필터
5. **`getReferenceTableColumns()`** - 라벨 정보 조회 (findMany → query)
- column_labels 조회
- 컬럼명과 라벨 매핑
### 주요 기술적 개선사항
- **information_schema 쿼리**: 파라미터 바인딩으로 변경 ($1, $2)
- **타입 안전성**: 명확한 반환 타입 지정
- **IS NOT NULL 조건**: Prisma의 { not: null } → IS NOT NULL
- **IN 조건**: 여러 데이터 타입 필터링
### 코드 정리
- [x] PrismaClient import 제거
- [x] import 문 수정 완료
- [x] TypeScript 컴파일 성공
- [x] Linter 오류 없음
## 📝 원본 전환 체크리스트
### 1단계: Prisma 호출 전환 (✅ 완료)
- [ ] `getEntityJoins()` - 목록 조회 (findMany with include)
- [ ] `getEntityJoin()` - 단건 조회 (findUnique)
- [ ] `createEntityJoin()` - 생성 (create with validation)
- [ ] `updateEntityJoin()` - 수정 (update)
- [ ] `deleteEntityJoin()` - 삭제 (delete)
### 2단계: 코드 정리
- [ ] import 문 수정 (`prisma` → `query, queryOne`)
- [ ] 조인 유효성 검증 로직 유지
- [ ] Prisma import 완전 제거
### 3단계: 테스트
- [ ] 단위 테스트 작성 (5개)
- [ ] 조인 유효성 검증 테스트
- [ ] 순환 참조 방지 테스트
- [ ] 통합 테스트 작성 (2개)
### 4단계: 문서화
- [ ] 전환 완료 문서 업데이트
---
## 🎯 예상 난이도 및 소요 시간
- **난이도**: ⭐⭐⭐ (중간)
- LEFT JOIN 쿼리
- 조인 유효성 검증
- 순환 참조 방지
- **예상 소요 시간**: 1시간
---
**상태**: ⏳ **대기 중**
**특이사항**: LEFT JOIN, 조인 유효성 검증, 순환 참조 방지 포함

View File

@ -1,456 +0,0 @@
# 📋 Phase 3.14: AuthService Raw Query 전환 계획
## 📋 개요
AuthService는 **5개의 Prisma 호출**이 있으며, 사용자 인증 및 권한 관리를 담당하는 서비스입니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ------------------------------------------ |
| 파일 위치 | `backend-node/src/services/authService.ts` |
| 파일 크기 | 335 라인 |
| Prisma 호출 | 0개 (이미 Phase 1.5에서 전환 완료) |
| **현재 진행률** | **5/5 (100%)****전환 완료** |
| 복잡도 | 높음 (보안, 암호화, 세션 관리) |
| 우선순위 | 🟡 중간 (Phase 3.14) |
| **상태** | ✅ **완료** (Phase 1.5에서 이미 완료) |
### 🎯 전환 목표
- ⏳ **5개 모든 Prisma 호출을 `db.ts``query()`, `queryOne()` 함수로 교체**
- ⏳ 사용자 인증 기능 정상 동작
- ⏳ 비밀번호 암호화/검증 유지
- ⏳ 세션 관리 기능 유지
- ⏳ 권한 검증 기능 유지
- ⏳ TypeScript 컴파일 성공
- ⏳ **Prisma import 완전 제거**
---
## 🔍 예상 Prisma 사용 패턴
### 주요 기능 (5개 예상)
#### 1. **사용자 로그인 (인증)**
- findFirst or findUnique
- 이메일/사용자명으로 조회
- 비밀번호 검증
#### 2. **사용자 정보 조회**
- findUnique
- user_id 기준
- 권한 정보 포함
#### 3. **사용자 생성 (회원가입)**
- create
- 비밀번호 암호화
- 중복 검사
#### 4. **비밀번호 변경**
- update
- 기존 비밀번호 검증
- 새 비밀번호 암호화
#### 5. **세션 관리**
- create, update, delete
- 세션 토큰 저장/조회
---
## 💡 전환 전략
### 1단계: 인증 관련 전환 (2개)
- login() - 사용자 조회 + 비밀번호 검증
- getUserInfo() - 사용자 정보 조회
### 2단계: 사용자 관리 전환 (2개)
- createUser() - 사용자 생성
- changePassword() - 비밀번호 변경
### 3단계: 세션 관리 전환 (1개)
- manageSession() - 세션 CRUD
---
## 💻 전환 예시
### 예시 1: 로그인 (비밀번호 검증)
**변경 전**:
```typescript
async login(username: string, password: string) {
const user = await prisma.users.findFirst({
where: {
OR: [
{ username: username },
{ email: username },
],
is_active: true,
},
});
if (!user) {
throw new Error("User not found");
}
const isPasswordValid = await bcrypt.compare(password, user.password_hash);
if (!isPasswordValid) {
throw new Error("Invalid password");
}
return user;
}
```
**변경 후**:
```typescript
async login(username: string, password: string) {
const user = await queryOne<any>(
`SELECT * FROM users
WHERE (username = $1 OR email = $1)
AND is_active = $2`,
[username, true]
);
if (!user) {
throw new Error("User not found");
}
const isPasswordValid = await bcrypt.compare(password, user.password_hash);
if (!isPasswordValid) {
throw new Error("Invalid password");
}
return user;
}
```
### 예시 2: 사용자 생성 (비밀번호 암호화)
**변경 전**:
```typescript
async createUser(userData: CreateUserDto) {
// 중복 검사
const existing = await prisma.users.findFirst({
where: {
OR: [
{ username: userData.username },
{ email: userData.email },
],
},
});
if (existing) {
throw new Error("User already exists");
}
// 비밀번호 암호화
const passwordHash = await bcrypt.hash(userData.password, 10);
// 사용자 생성
const user = await prisma.users.create({
data: {
username: userData.username,
email: userData.email,
password_hash: passwordHash,
company_code: userData.company_code,
},
});
return user;
}
```
**변경 후**:
```typescript
async createUser(userData: CreateUserDto) {
// 중복 검사
const existing = await queryOne<any>(
`SELECT * FROM users
WHERE username = $1 OR email = $2`,
[userData.username, userData.email]
);
if (existing) {
throw new Error("User already exists");
}
// 비밀번호 암호화
const passwordHash = await bcrypt.hash(userData.password, 10);
// 사용자 생성
const user = await queryOne<any>(
`INSERT INTO users
(username, email, password_hash, company_code, created_at, updated_at)
VALUES ($1, $2, $3, $4, NOW(), NOW())
RETURNING *`,
[userData.username, userData.email, passwordHash, userData.company_code]
);
return user;
}
```
### 예시 3: 비밀번호 변경
**변경 전**:
```typescript
async changePassword(
userId: number,
oldPassword: string,
newPassword: string
) {
const user = await prisma.users.findUnique({
where: { user_id: userId },
});
if (!user) {
throw new Error("User not found");
}
const isOldPasswordValid = await bcrypt.compare(
oldPassword,
user.password_hash
);
if (!isOldPasswordValid) {
throw new Error("Invalid old password");
}
const newPasswordHash = await bcrypt.hash(newPassword, 10);
await prisma.users.update({
where: { user_id: userId },
data: { password_hash: newPasswordHash },
});
}
```
**변경 후**:
```typescript
async changePassword(
userId: number,
oldPassword: string,
newPassword: string
) {
const user = await queryOne<any>(
`SELECT * FROM users WHERE user_id = $1`,
[userId]
);
if (!user) {
throw new Error("User not found");
}
const isOldPasswordValid = await bcrypt.compare(
oldPassword,
user.password_hash
);
if (!isOldPasswordValid) {
throw new Error("Invalid old password");
}
const newPasswordHash = await bcrypt.hash(newPassword, 10);
await query(
`UPDATE users
SET password_hash = $1, updated_at = NOW()
WHERE user_id = $2`,
[newPasswordHash, userId]
);
}
```
---
## 🔧 기술적 고려사항
### 1. 비밀번호 보안
```typescript
import bcrypt from "bcrypt";
// 비밀번호 해싱 (회원가입, 비밀번호 변경)
const SALT_ROUNDS = 10;
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
// 비밀번호 검증 (로그인)
const isValid = await bcrypt.compare(plainPassword, passwordHash);
```
### 2. SQL 인젝션 방지
```typescript
// ❌ 위험: 직접 문자열 결합
const sql = `SELECT * FROM users WHERE username = '${username}'`;
// ✅ 안전: 파라미터 바인딩
const user = await queryOne(`SELECT * FROM users WHERE username = $1`, [
username,
]);
```
### 3. 세션 토큰 관리
```typescript
import crypto from "crypto";
// 세션 토큰 생성
const sessionToken = crypto.randomBytes(32).toString("hex");
// 세션 저장
await query(
`INSERT INTO user_sessions (user_id, session_token, expires_at)
VALUES ($1, $2, NOW() + INTERVAL '1 day')`,
[userId, sessionToken]
);
```
### 4. 권한 검증
```typescript
async checkPermission(userId: number, permission: string): Promise<boolean> {
const result = await queryOne<{ has_permission: boolean }>(
`SELECT EXISTS (
SELECT 1 FROM user_permissions up
JOIN permissions p ON up.permission_id = p.permission_id
WHERE up.user_id = $1 AND p.permission_name = $2
) as has_permission`,
[userId, permission]
);
return result?.has_permission || false;
}
```
---
## ✅ 전환 완료 내역 (Phase 1.5에서 이미 완료됨)
AuthService는 Phase 1.5에서 이미 Raw Query로 전환이 완료되었습니다.
### 전환된 Prisma 호출 (5개)
1. **`loginPwdCheck()`** - 로그인 비밀번호 검증
- user_info 테이블에서 비밀번호 조회
- EncryptUtil을 활용한 비밀번호 검증
- 마스터 패스워드 지원
2. **`insertLoginAccessLog()`** - 로그인 로그 기록
- login_access_log 테이블에 INSERT
- 로그인 시간, IP 주소 등 기록
3. **`getUserInfo()`** - 사용자 정보 조회
- user_info 테이블 조회
- PersonBean 객체로 반환
4. **`updateLastLoginDate()`** - 마지막 로그인 시간 업데이트
- user_info 테이블 UPDATE
- last_login_date 갱신
5. **`checkUserPermission()`** - 사용자 권한 확인
- user_auth 테이블 조회
- 권한 코드 검증
### 주요 기술적 특징
- **보안**: EncryptUtil을 활용한 안전한 비밀번호 검증
- **JWT 토큰**: JwtUtils를 활용한 토큰 생성 및 검증
- **로깅**: 상세한 로그인 이력 기록
- **에러 처리**: 안전한 에러 메시지 반환
### 코드 상태
- [x] Prisma import 없음
- [x] query 함수 사용 중
- [x] TypeScript 컴파일 성공
- [x] 보안 로직 유지
## 📝 원본 전환 체크리스트
### 1단계: Prisma 호출 전환 (✅ Phase 1.5에서 완료)
- [ ] `login()` - 사용자 조회 + 비밀번호 검증 (findFirst)
- [ ] `getUserInfo()` - 사용자 정보 조회 (findUnique)
- [ ] `createUser()` - 사용자 생성 (create with 중복 검사)
- [ ] `changePassword()` - 비밀번호 변경 (findUnique + update)
- [ ] `manageSession()` - 세션 관리 (create/update/delete)
### 2단계: 보안 검증
- [ ] 비밀번호 해싱 로직 유지 (bcrypt)
- [ ] SQL 인젝션 방지 확인
- [ ] 세션 토큰 보안 확인
- [ ] 중복 계정 방지 확인
### 3단계: 테스트
- [ ] 단위 테스트 작성 (5개)
- [ ] 로그인 성공/실패 테스트
- [ ] 사용자 생성 테스트
- [ ] 비밀번호 변경 테스트
- [ ] 세션 관리 테스트
- [ ] 권한 검증 테스트
- [ ] 보안 테스트
- [ ] SQL 인젝션 테스트
- [ ] 비밀번호 강도 테스트
- [ ] 세션 탈취 방지 테스트
- [ ] 통합 테스트 작성 (2개)
### 4단계: 문서화
- [ ] 전환 완료 문서 업데이트
- [ ] 보안 가이드 업데이트
---
## 🎯 예상 난이도 및 소요 시간
- **난이도**: ⭐⭐⭐⭐ (높음)
- 보안 크리티컬 (비밀번호, 세션)
- SQL 인젝션 방지 필수
- 철저한 테스트 필요
- **예상 소요 시간**: 1.5~2시간
- Prisma 호출 전환: 40분
- 보안 검증: 40분
- 테스트: 40분
---
## ⚠️ 주의사항
### 보안 필수 체크리스트
1. ✅ 모든 사용자 입력은 파라미터 바인딩 사용
2. ✅ 비밀번호는 절대 평문 저장 금지 (bcrypt 사용)
3. ✅ 세션 토큰은 충분히 길고 랜덤해야 함
4. ✅ 비밀번호 실패 시 구체적 오류 메시지 금지 ("User not found" vs "Invalid credentials")
5. ✅ 로그인 실패 횟수 제한 (Brute Force 방지)
---
**상태**: ⏳ **대기 중**
**특이사항**: 보안 크리티컬, 비밀번호 암호화, 세션 관리 포함
**⚠️ 주의**: 이 서비스는 보안에 매우 중요하므로 신중한 테스트 필수!

View File

@ -1,515 +0,0 @@
# 📋 Phase 3.15: Batch Services Raw Query 전환 계획
## 📋 개요
배치 관련 서비스들은 총 **24개의 Prisma 호출**이 있으며, 배치 작업 실행 및 관리를 담당합니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ---------------------------------------------------------- |
| 대상 서비스 | 4개 (BatchExternalDb, ExecutionLog, Management, Scheduler) |
| 파일 위치 | `backend-node/src/services/batch*.ts` |
| 총 파일 크기 | 2,161 라인 |
| Prisma 호출 | 0개 (전환 완료) |
| **현재 진행률** | **24/24 (100%)****전환 완료** |
| 복잡도 | 높음 (외부 DB 연동, 스케줄링, 트랜잭션) |
| 우선순위 | 🔴 높음 (Phase 3.15) |
| **상태** | ✅ **완료** |
---
## ✅ 전환 완료 내역
### 전환된 Prisma 호출 (24개)
#### 1. BatchExternalDbService (8개)
- `getAvailableConnections()` - findMany → query
- `getTables()` - $queryRaw → query (information_schema)
- `getTableColumns()` - $queryRaw → query (information_schema)
- `getExternalTables()` - findUnique → queryOne (x5)
#### 2. BatchExecutionLogService (7개)
- `getExecutionLogs()` - findMany + count → query (JOIN + 동적 WHERE)
- `createExecutionLog()` - create → queryOne (INSERT RETURNING)
- `updateExecutionLog()` - update → queryOne (동적 UPDATE)
- `deleteExecutionLog()` - delete → query
- `getLatestExecutionLog()` - findFirst → queryOne
- `getExecutionStats()` - findMany → query (동적 WHERE)
#### 3. BatchManagementService (5개)
- `getAvailableConnections()` - findMany → query
- `getTables()` - $queryRaw → query (information_schema)
- `getTableColumns()` - $queryRaw → query (information_schema)
- `getExternalTables()` - findUnique → queryOne (x2)
#### 4. BatchSchedulerService (4개)
- `loadActiveBatchConfigs()` - findMany → query (JOIN with json_agg)
- `updateBatchSchedule()` - findUnique → query (JOIN with json_agg)
- `getDataFromSource()` - $queryRawUnsafe → query
- `insertDataToTarget()` - $executeRawUnsafe → query
### 주요 기술적 해결 사항
1. **외부 DB 연결 조회 반복**
- 5개의 `findUnique` 호출을 `queryOne`으로 일괄 전환
- 암호화/복호화 로직 유지
2. **배치 설정 + 매핑 JOIN**
- Prisma `include``json_agg` + `json_build_object`
- `FILTER (WHERE bm.id IS NOT NULL)` 로 NULL 방지
- 계층적 JSON 데이터 생성
3. **동적 WHERE 절 생성**
- 조건부 필터링 (batch_config_id, execution_status, 날짜 범위)
- 파라미터 인덱스 동적 관리
4. **동적 UPDATE 쿼리**
- undefined 필드 제외
- 8개 필드의 조건부 업데이트
5. **통계 쿼리 전환**
- 클라이언트 사이드 집계 유지
- 원본 데이터만 쿼리로 조회
### 컴파일 상태
✅ TypeScript 컴파일 성공
✅ Linter 오류 없음
---
## 🔍 서비스별 상세 분석
### 1. BatchExternalDbService (8개 호출, 943 라인)
**주요 기능**:
- 외부 DB에서 배치 데이터 조회
- 외부 DB로 배치 데이터 저장
- 외부 DB 연결 관리
- 데이터 변환 및 매핑
**예상 Prisma 호출**:
- `getExternalDbConnection()` - 외부 DB 연결 정보 조회
- `fetchDataFromExternalDb()` - 외부 DB 데이터 조회
- `saveDataToExternalDb()` - 외부 DB 데이터 저장
- `validateExternalDbConnection()` - 연결 검증
- `getExternalDbTables()` - 테이블 목록 조회
- `getExternalDbColumns()` - 컬럼 정보 조회
- `executeBatchQuery()` - 배치 쿼리 실행
- `getBatchExecutionStatus()` - 실행 상태 조회
**기술적 고려사항**:
- 다양한 DB 타입 지원 (PostgreSQL, MySQL, Oracle, MSSQL)
- 연결 풀 관리
- 트랜잭션 처리
- 에러 핸들링 및 재시도
---
### 2. BatchExecutionLogService (7개 호출, 299 라인)
**주요 기능**:
- 배치 실행 로그 생성
- 배치 실행 이력 조회
- 배치 실행 통계
- 로그 정리
**예상 Prisma 호출**:
- `createExecutionLog()` - 실행 로그 생성
- `updateExecutionLog()` - 실행 로그 업데이트
- `getExecutionLogs()` - 실행 로그 목록 조회
- `getExecutionLogById()` - 실행 로그 단건 조회
- `getExecutionStats()` - 실행 통계 조회
- `cleanupOldLogs()` - 오래된 로그 삭제
- `getFailedExecutions()` - 실패한 실행 조회
**기술적 고려사항**:
- 대용량 로그 처리
- 통계 쿼리 최적화
- 로그 보관 정책
- 페이징 및 필터링
---
### 3. BatchManagementService (5개 호출, 373 라인)
**주요 기능**:
- 배치 작업 설정 관리
- 배치 작업 실행
- 배치 작업 중지
- 배치 작업 모니터링
**예상 Prisma 호출**:
- `getBatchJobs()` - 배치 작업 목록 조회
- `getBatchJob()` - 배치 작업 단건 조회
- `createBatchJob()` - 배치 작업 생성
- `updateBatchJob()` - 배치 작업 수정
- `deleteBatchJob()` - 배치 작업 삭제
**기술적 고려사항**:
- JSON 설정 필드 (job_config)
- 작업 상태 관리
- 동시 실행 제어
- 의존성 관리
---
### 4. BatchSchedulerService (4개 호출, 546 라인)
**주요 기능**:
- 배치 스케줄 설정
- Cron 표현식 관리
- 스케줄 실행
- 다음 실행 시간 계산
**예상 Prisma 호출**:
- `getScheduledBatches()` - 스케줄된 배치 조회
- `createSchedule()` - 스케줄 생성
- `updateSchedule()` - 스케줄 수정
- `deleteSchedule()` - 스케줄 삭제
**기술적 고려사항**:
- Cron 표현식 파싱
- 시간대 처리
- 실행 이력 추적
- 스케줄 충돌 방지
---
## 💡 통합 전환 전략
### Phase 1: 핵심 서비스 전환 (12개)
**BatchManagementService (5개) + BatchExecutionLogService (7개)**
- 배치 관리 및 로깅 기능 우선
- 상대적으로 단순한 CRUD
### Phase 2: 스케줄러 전환 (4개)
**BatchSchedulerService (4개)**
- 스케줄 관리
- Cron 표현식 처리
### Phase 3: 외부 DB 연동 전환 (8개)
**BatchExternalDbService (8개)**
- 가장 복잡한 서비스
- 외부 DB 연결 및 쿼리
---
## 💻 전환 예시
### 예시 1: 배치 실행 로그 생성
**변경 전**:
```typescript
const log = await prisma.batch_execution_logs.create({
data: {
batch_id: batchId,
status: "running",
started_at: new Date(),
execution_params: params,
company_code: companyCode,
},
});
```
**변경 후**:
```typescript
const log = await queryOne<any>(
`INSERT INTO batch_execution_logs
(batch_id, status, started_at, execution_params, company_code)
VALUES ($1, $2, NOW(), $3, $4)
RETURNING *`,
[batchId, "running", JSON.stringify(params), companyCode]
);
```
### 예시 2: 배치 통계 조회
**변경 전**:
```typescript
const stats = await prisma.batch_execution_logs.groupBy({
by: ["status"],
where: {
batch_id: batchId,
started_at: { gte: startDate, lte: endDate },
},
_count: { id: true },
});
```
**변경 후**:
```typescript
const stats = await query<{ status: string; count: string }>(
`SELECT status, COUNT(*) as count
FROM batch_execution_logs
WHERE batch_id = $1
AND started_at >= $2
AND started_at <= $3
GROUP BY status`,
[batchId, startDate, endDate]
);
```
### 예시 3: 외부 DB 연결 및 쿼리
**변경 전**:
```typescript
// 연결 정보 조회
const connection = await prisma.external_db_connections.findUnique({
where: { id: connectionId },
});
// 외부 DB 쿼리 실행 (Prisma 사용 불가, 이미 Raw Query일 가능성)
const externalData = await externalDbClient.query(sql);
```
**변경 후**:
```typescript
// 연결 정보 조회
const connection = await queryOne<any>(
`SELECT * FROM external_db_connections WHERE id = $1`,
[connectionId]
);
// 외부 DB 쿼리 실행 (기존 로직 유지)
const externalData = await externalDbClient.query(sql);
```
### 예시 4: 스케줄 관리
**변경 전**:
```typescript
const schedule = await prisma.batch_schedules.create({
data: {
batch_id: batchId,
cron_expression: cronExp,
is_active: true,
next_run_at: calculateNextRun(cronExp),
},
});
```
**변경 후**:
```typescript
const nextRun = calculateNextRun(cronExp);
const schedule = await queryOne<any>(
`INSERT INTO batch_schedules
(batch_id, cron_expression, is_active, next_run_at, created_at, updated_at)
VALUES ($1, $2, $3, $4, NOW(), NOW())
RETURNING *`,
[batchId, cronExp, true, nextRun]
);
```
---
## 🔧 기술적 고려사항
### 1. 외부 DB 연결 관리
```typescript
import { DatabaseConnectorFactory } from "../database/connectorFactory";
// 외부 DB 연결 생성
const connector = DatabaseConnectorFactory.create(connection);
const externalClient = await connector.connect();
try {
// 쿼리 실행
const result = await externalClient.query(sql, params);
} finally {
await connector.disconnect();
}
```
### 2. 트랜잭션 처리
```typescript
await transaction(async (client) => {
// 배치 상태 업데이트
await client.query(`UPDATE batch_jobs SET status = $1 WHERE id = $2`, [
"running",
batchId,
]);
// 실행 로그 생성
await client.query(
`INSERT INTO batch_execution_logs (batch_id, status, started_at)
VALUES ($1, $2, NOW())`,
[batchId, "running"]
);
});
```
### 3. Cron 표현식 처리
```typescript
import cron from "node-cron";
// Cron 표현식 검증
const isValid = cron.validate(cronExpression);
// 다음 실행 시간 계산
function calculateNextRun(cronExp: string): Date {
// Cron 파서를 사용하여 다음 실행 시간 계산
// ...
}
```
### 4. 대용량 데이터 처리
```typescript
// 스트리밍 방식으로 대용량 데이터 처리
const stream = await query<any>(
`SELECT * FROM large_table WHERE batch_id = $1`,
[batchId]
);
for await (const row of stream) {
// 행 단위 처리
}
```
---
## 📝 전환 체크리스트
### BatchExternalDbService (8개)
- [ ] `getExternalDbConnection()` - 연결 정보 조회
- [ ] `fetchDataFromExternalDb()` - 외부 DB 데이터 조회
- [ ] `saveDataToExternalDb()` - 외부 DB 데이터 저장
- [ ] `validateExternalDbConnection()` - 연결 검증
- [ ] `getExternalDbTables()` - 테이블 목록 조회
- [ ] `getExternalDbColumns()` - 컬럼 정보 조회
- [ ] `executeBatchQuery()` - 배치 쿼리 실행
- [ ] `getBatchExecutionStatus()` - 실행 상태 조회
### BatchExecutionLogService (7개)
- [ ] `createExecutionLog()` - 실행 로그 생성
- [ ] `updateExecutionLog()` - 실행 로그 업데이트
- [ ] `getExecutionLogs()` - 실행 로그 목록 조회
- [ ] `getExecutionLogById()` - 실행 로그 단건 조회
- [ ] `getExecutionStats()` - 실행 통계 조회
- [ ] `cleanupOldLogs()` - 오래된 로그 삭제
- [ ] `getFailedExecutions()` - 실패한 실행 조회
### BatchManagementService (5개)
- [ ] `getBatchJobs()` - 배치 작업 목록 조회
- [ ] `getBatchJob()` - 배치 작업 단건 조회
- [ ] `createBatchJob()` - 배치 작업 생성
- [ ] `updateBatchJob()` - 배치 작업 수정
- [ ] `deleteBatchJob()` - 배치 작업 삭제
### BatchSchedulerService (4개)
- [ ] `getScheduledBatches()` - 스케줄된 배치 조회
- [ ] `createSchedule()` - 스케줄 생성
- [ ] `updateSchedule()` - 스케줄 수정
- [ ] `deleteSchedule()` - 스케줄 삭제
### 공통 작업
- [ ] import 문 수정 (모든 서비스)
- [ ] Prisma import 완전 제거 (모든 서비스)
- [ ] 트랜잭션 로직 확인
- [ ] 에러 핸들링 검증
---
## 🧪 테스트 계획
### 단위 테스트 (24개)
- 각 Prisma 호출별 1개씩
### 통합 테스트 (8개)
- BatchExternalDbService: 외부 DB 연동 테스트 (2개)
- BatchExecutionLogService: 로그 생성 및 조회 테스트 (2개)
- BatchManagementService: 배치 작업 실행 테스트 (2개)
- BatchSchedulerService: 스케줄 실행 테스트 (2개)
### 성능 테스트
- 대용량 데이터 처리 성능
- 동시 배치 실행 성능
- 외부 DB 연결 풀 성능
---
## 🎯 예상 난이도 및 소요 시간
- **난이도**: ⭐⭐⭐⭐⭐ (매우 높음)
- 외부 DB 연동
- 트랜잭션 처리
- 스케줄링 로직
- 대용량 데이터 처리
- **예상 소요 시간**: 4~5시간
- Phase 1 (BatchManagement + ExecutionLog): 1.5시간
- Phase 2 (Scheduler): 1시간
- Phase 3 (ExternalDb): 2시간
- 테스트 및 문서화: 0.5시간
---
## ⚠️ 주의사항
### 중요 체크포인트
1. ✅ 외부 DB 연결은 반드시 try-finally에서 해제
2. ✅ 배치 실행 중 에러 시 롤백 처리
3. ✅ Cron 표현식 검증 필수
4. ✅ 대용량 데이터는 스트리밍 방식 사용
5. ✅ 동시 실행 제한 확인
### 성능 최적화
- 연결 풀 활용
- 배치 쿼리 최적화
- 인덱스 확인
- 불필요한 로그 제거
---
**상태**: ⏳ **대기 중**
**특이사항**: 외부 DB 연동, 스케줄링, 트랜잭션 처리 포함
**⚠️ 주의**: 배치 시스템의 핵심 기능이므로 신중한 테스트 필수!

View File

@ -1,540 +0,0 @@
# 📋 Phase 3.16: Data Management Services Raw Query 전환 계획
## 📋 개요
데이터 관리 관련 서비스들은 총 **18개의 Prisma 호출**이 있으며, 동적 폼, 데이터 매핑, 데이터 서비스, 관리자 기능을 담당합니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ----------------------------------------------------- |
| 대상 서비스 | 4개 (EnhancedDynamicForm, DataMapping, Data, Admin) |
| 파일 위치 | `backend-node/src/services/{enhanced,data,admin}*.ts` |
| 총 파일 크기 | 2,062 라인 |
| Prisma 호출 | 0개 (전환 완료) |
| **현재 진행률** | **18/18 (100%)****전환 완료** |
| 복잡도 | 중간 (동적 쿼리, JSON 필드, 관리자 기능) |
| 우선순위 | 🟡 중간 (Phase 3.16) |
| **상태** | ✅ **완료** |
---
## ✅ 전환 완료 내역
### 전환된 Prisma 호출 (18개)
#### 1. EnhancedDynamicFormService (6개)
- `validateTableExists()` - $queryRawUnsafe → query
- `getTableColumns()` - $queryRawUnsafe → query
- `getColumnWebTypes()` - $queryRawUnsafe → query
- `getPrimaryKeys()` - $queryRawUnsafe → query
- `performInsert()` - $queryRawUnsafe → query
- `performUpdate()` - $queryRawUnsafe → query
#### 2. DataMappingService (5개)
- `getSourceData()` - $queryRawUnsafe → query
- `executeInsert()` - $executeRawUnsafe → query
- `executeUpsert()` - $executeRawUnsafe → query
- `executeUpdate()` - $executeRawUnsafe → query
- `disconnect()` - 제거 (Raw Query는 disconnect 불필요)
#### 3. DataService (4개)
- `getTableData()` - $queryRawUnsafe → query
- `checkTableExists()` - $queryRawUnsafe → query
- `getTableColumnsSimple()` - $queryRawUnsafe → query
- `getColumnLabel()` - $queryRawUnsafe → query
#### 4. AdminService (3개)
- `getAdminMenuList()` - $queryRaw → query (WITH RECURSIVE)
- `getUserMenuList()` - $queryRaw → query (WITH RECURSIVE)
- `getMenuInfo()` - findUnique → query (JOIN)
### 주요 기술적 해결 사항
1. **변수명 충돌 해결**
- `dataService.ts`에서 `query` 변수 → `sql` 변수로 변경
- `query()` 함수와 로컬 변수 충돌 방지
2. **WITH RECURSIVE 쿼리 전환**
- Prisma의 `$queryRaw` 템플릿 리터럴 → 일반 문자열
- `${userLang}``$1` 파라미터 바인딩
3. **JOIN 쿼리 전환**
- Prisma의 `include` 옵션 → `LEFT JOIN` 쿼리
- 관계 데이터를 단일 쿼리로 조회
4. **동적 쿼리 생성**
- 동적 WHERE 조건 구성
- SQL 인젝션 방지 (컬럼명 검증)
- 동적 ORDER BY 처리
### 컴파일 상태
✅ TypeScript 컴파일 성공
✅ Linter 오류 없음
---
## 🔍 서비스별 상세 분석
### 1. EnhancedDynamicFormService (6개 호출, 786 라인)
**주요 기능**:
- 고급 동적 폼 관리
- 폼 검증 규칙
- 조건부 필드 표시
- 폼 템플릿 관리
**예상 Prisma 호출**:
- `getEnhancedForms()` - 고급 폼 목록 조회
- `getEnhancedForm()` - 고급 폼 단건 조회
- `createEnhancedForm()` - 고급 폼 생성
- `updateEnhancedForm()` - 고급 폼 수정
- `deleteEnhancedForm()` - 고급 폼 삭제
- `getFormValidationRules()` - 검증 규칙 조회
**기술적 고려사항**:
- JSON 필드 (validation_rules, conditional_logic, field_config)
- 복잡한 검증 규칙
- 동적 필드 생성
- 조건부 표시 로직
---
### 2. DataMappingService (5개 호출, 575 라인)
**주요 기능**:
- 데이터 매핑 설정 관리
- 소스-타겟 필드 매핑
- 데이터 변환 규칙
- 매핑 실행
**예상 Prisma 호출**:
- `getDataMappings()` - 매핑 설정 목록 조회
- `getDataMapping()` - 매핑 설정 단건 조회
- `createDataMapping()` - 매핑 설정 생성
- `updateDataMapping()` - 매핑 설정 수정
- `deleteDataMapping()` - 매핑 설정 삭제
**기술적 고려사항**:
- JSON 필드 (field_mappings, transformation_rules)
- 복잡한 변환 로직
- 매핑 검증
- 실행 이력 추적
---
### 3. DataService (4개 호출, 327 라인)
**주요 기능**:
- 동적 데이터 조회
- 데이터 필터링
- 데이터 정렬
- 데이터 집계
**예상 Prisma 호출**:
- `getDataByTable()` - 테이블별 데이터 조회
- `getDataById()` - 데이터 단건 조회
- `executeCustomQuery()` - 커스텀 쿼리 실행
- `getDataStatistics()` - 데이터 통계 조회
**기술적 고려사항**:
- 동적 테이블 쿼리
- SQL 인젝션 방지
- 동적 WHERE 조건
- 집계 쿼리
---
### 4. AdminService (3개 호출, 374 라인)
**주요 기능**:
- 관리자 메뉴 관리
- 시스템 설정
- 사용자 관리
- 로그 조회
**예상 Prisma 호출**:
- `getAdminMenus()` - 관리자 메뉴 조회
- `getSystemSettings()` - 시스템 설정 조회
- `updateSystemSettings()` - 시스템 설정 업데이트
**기술적 고려사항**:
- 메뉴 계층 구조
- 권한 기반 필터링
- JSON 설정 필드
- 캐싱
---
## 💡 통합 전환 전략
### Phase 1: 단순 CRUD 전환 (12개)
**EnhancedDynamicFormService (6개) + DataMappingService (5개) + AdminService (1개)**
- 기본 CRUD 기능
- JSON 필드 처리
### Phase 2: 동적 쿼리 전환 (4개)
**DataService (4개)**
- 동적 테이블 쿼리
- 보안 검증
### Phase 3: 고급 기능 전환 (2개)
**AdminService (2개)**
- 시스템 설정
- 캐싱
---
## 💻 전환 예시
### 예시 1: 고급 폼 생성 (JSON 필드)
**변경 전**:
```typescript
const form = await prisma.enhanced_forms.create({
data: {
form_code: formCode,
form_name: formName,
validation_rules: validationRules, // JSON
conditional_logic: conditionalLogic, // JSON
field_config: fieldConfig, // JSON
company_code: companyCode,
},
});
```
**변경 후**:
```typescript
const form = await queryOne<any>(
`INSERT INTO enhanced_forms
(form_code, form_name, validation_rules, conditional_logic,
field_config, company_code, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
RETURNING *`,
[
formCode,
formName,
JSON.stringify(validationRules),
JSON.stringify(conditionalLogic),
JSON.stringify(fieldConfig),
companyCode,
]
);
```
### 예시 2: 데이터 매핑 조회
**변경 전**:
```typescript
const mappings = await prisma.data_mappings.findMany({
where: {
source_table: sourceTable,
target_table: targetTable,
is_active: true,
},
include: {
source_columns: true,
target_columns: true,
},
});
```
**변경 후**:
```typescript
const mappings = await query<any>(
`SELECT
dm.*,
json_agg(DISTINCT jsonb_build_object(
'column_id', sc.column_id,
'column_name', sc.column_name
)) FILTER (WHERE sc.column_id IS NOT NULL) as source_columns,
json_agg(DISTINCT jsonb_build_object(
'column_id', tc.column_id,
'column_name', tc.column_name
)) FILTER (WHERE tc.column_id IS NOT NULL) as target_columns
FROM data_mappings dm
LEFT JOIN columns sc ON dm.mapping_id = sc.mapping_id AND sc.type = 'source'
LEFT JOIN columns tc ON dm.mapping_id = tc.mapping_id AND tc.type = 'target'
WHERE dm.source_table = $1
AND dm.target_table = $2
AND dm.is_active = $3
GROUP BY dm.mapping_id`,
[sourceTable, targetTable, true]
);
```
### 예시 3: 동적 테이블 쿼리 (DataService)
**변경 전**:
```typescript
// Prisma로는 동적 테이블 쿼리 불가능
// 이미 $queryRawUnsafe 사용 중일 가능성
const data = await prisma.$queryRawUnsafe(
`SELECT * FROM ${tableName} WHERE ${whereClause}`,
...params
);
```
**변경 후**:
```typescript
// SQL 인젝션 방지를 위한 테이블명 검증
const validTableName = validateTableName(tableName);
const data = await query<any>(
`SELECT * FROM ${validTableName} WHERE ${whereClause}`,
params
);
```
### 예시 4: 관리자 메뉴 조회 (계층 구조)
**변경 전**:
```typescript
const menus = await prisma.admin_menus.findMany({
where: { is_active: true },
orderBy: { sort_order: "asc" },
include: {
children: {
orderBy: { sort_order: "asc" },
},
},
});
```
**변경 후**:
```typescript
// 재귀 CTE를 사용한 계층 쿼리
const menus = await query<any>(
`WITH RECURSIVE menu_tree AS (
SELECT *, 0 as level, ARRAY[menu_id] as path
FROM admin_menus
WHERE parent_id IS NULL AND is_active = $1
UNION ALL
SELECT m.*, mt.level + 1, mt.path || m.menu_id
FROM admin_menus m
JOIN menu_tree mt ON m.parent_id = mt.menu_id
WHERE m.is_active = $1
)
SELECT * FROM menu_tree
ORDER BY path, sort_order`,
[true]
);
```
---
## 🔧 기술적 고려사항
### 1. JSON 필드 처리
```typescript
// 복잡한 JSON 구조
interface ValidationRules {
required?: string[];
min?: Record<string, number>;
max?: Record<string, number>;
pattern?: Record<string, string>;
custom?: Array<{ field: string; rule: string }>;
}
// 저장 시
JSON.stringify(validationRules);
// 조회 후
const parsed =
typeof row.validation_rules === "string"
? JSON.parse(row.validation_rules)
: row.validation_rules;
```
### 2. 동적 테이블 쿼리 보안
```typescript
// 테이블명 화이트리스트
const ALLOWED_TABLES = ["users", "products", "orders"];
function validateTableName(tableName: string): string {
if (!ALLOWED_TABLES.includes(tableName)) {
throw new Error("Invalid table name");
}
return tableName;
}
// 컬럼명 검증
function validateColumnName(columnName: string): string {
if (!/^[a-z_][a-z0-9_]*$/i.test(columnName)) {
throw new Error("Invalid column name");
}
return columnName;
}
```
### 3. 재귀 CTE (계층 구조)
```sql
WITH RECURSIVE hierarchy AS (
-- 최상위 노드
SELECT * FROM table WHERE parent_id IS NULL
UNION ALL
-- 하위 노드
SELECT t.* FROM table t
JOIN hierarchy h ON t.parent_id = h.id
)
SELECT * FROM hierarchy
```
### 4. JSON 집계 (관계 데이터)
```sql
SELECT
parent.*,
COALESCE(
json_agg(
jsonb_build_object('id', child.id, 'name', child.name)
) FILTER (WHERE child.id IS NOT NULL),
'[]'
) as children
FROM parent
LEFT JOIN child ON parent.id = child.parent_id
GROUP BY parent.id
```
---
## 📝 전환 체크리스트
### EnhancedDynamicFormService (6개)
- [ ] `getEnhancedForms()` - 목록 조회
- [ ] `getEnhancedForm()` - 단건 조회
- [ ] `createEnhancedForm()` - 생성 (JSON 필드)
- [ ] `updateEnhancedForm()` - 수정 (JSON 필드)
- [ ] `deleteEnhancedForm()` - 삭제
- [ ] `getFormValidationRules()` - 검증 규칙 조회
### DataMappingService (5개)
- [ ] `getDataMappings()` - 목록 조회
- [ ] `getDataMapping()` - 단건 조회
- [ ] `createDataMapping()` - 생성
- [ ] `updateDataMapping()` - 수정
- [ ] `deleteDataMapping()` - 삭제
### DataService (4개)
- [ ] `getDataByTable()` - 동적 테이블 조회
- [ ] `getDataById()` - 단건 조회
- [ ] `executeCustomQuery()` - 커스텀 쿼리
- [ ] `getDataStatistics()` - 통계 조회
### AdminService (3개)
- [ ] `getAdminMenus()` - 메뉴 조회 (재귀 CTE)
- [ ] `getSystemSettings()` - 시스템 설정 조회
- [ ] `updateSystemSettings()` - 시스템 설정 업데이트
### 공통 작업
- [ ] import 문 수정 (모든 서비스)
- [ ] Prisma import 완전 제거
- [ ] JSON 필드 처리 확인
- [ ] 보안 검증 (SQL 인젝션)
---
## 🧪 테스트 계획
### 단위 테스트 (18개)
- 각 Prisma 호출별 1개씩
### 통합 테스트 (6개)
- EnhancedDynamicFormService: 폼 생성 및 검증 테스트 (2개)
- DataMappingService: 매핑 설정 및 실행 테스트 (2개)
- DataService: 동적 쿼리 및 보안 테스트 (1개)
- AdminService: 메뉴 계층 구조 테스트 (1개)
### 보안 테스트
- SQL 인젝션 방지 테스트
- 테이블명 검증 테스트
- 컬럼명 검증 테스트
---
## 🎯 예상 난이도 및 소요 시간
- **난이도**: ⭐⭐⭐⭐ (높음)
- JSON 필드 처리
- 동적 쿼리 보안
- 재귀 CTE
- JSON 집계
- **예상 소요 시간**: 2.5~3시간
- Phase 1 (기본 CRUD): 1시간
- Phase 2 (동적 쿼리): 1시간
- Phase 3 (고급 기능): 0.5시간
- 테스트 및 문서화: 0.5시간
---
## ⚠️ 주의사항
### 보안 필수 체크리스트
1. ✅ 동적 테이블명은 반드시 화이트리스트 검증
2. ✅ 동적 컬럼명은 정규식으로 검증
3. ✅ WHERE 절 파라미터는 반드시 바인딩
4. ✅ JSON 필드는 파싱 에러 처리
5. ✅ 재귀 쿼리는 깊이 제한 설정
### 성능 최적화
- JSON 필드 인덱싱 (GIN 인덱스)
- 재귀 쿼리 깊이 제한
- 집계 쿼리 최적화
- 필요시 캐싱 적용
---
**상태**: ⏳ **대기 중**
**특이사항**: JSON 필드, 동적 쿼리, 재귀 CTE, 보안 검증 포함
**⚠️ 주의**: 동적 쿼리는 SQL 인젝션 방지가 매우 중요!

View File

@ -1,62 +0,0 @@
# 📋 Phase 3.17: ReferenceCacheService Raw Query 전환 계획
## 📋 개요
ReferenceCacheService는 **0개의 Prisma 호출**이 있으며, 참조 데이터 캐싱을 담당하는 서비스입니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ---------------------------------------------------- |
| 파일 위치 | `backend-node/src/services/referenceCacheService.ts` |
| 파일 크기 | 499 라인 |
| Prisma 호출 | 0개 (이미 전환 완료) |
| **현재 진행률** | **3/3 (100%)****전환 완료** |
| 복잡도 | 낮음 (캐싱 로직) |
| 우선순위 | 🟢 낮음 (Phase 3.17) |
| **상태** | ✅ **완료** (이미 전환 완료됨) |
---
## ✅ 전환 완료 내역 (이미 완료됨)
ReferenceCacheService는 이미 Raw Query로 전환이 완료되었습니다.
### 주요 기능
1. **참조 데이터 캐싱**
- 자주 사용되는 참조 테이블 데이터를 메모리에 캐싱
- 성능 향상을 위한 캐시 전략
2. **캐시 관리**
- 캐시 갱신 로직
- TTL(Time To Live) 관리
- 캐시 무효화
3. **데이터 조회 최적화**
- 캐시 히트/미스 처리
- 백그라운드 갱신
### 기술적 특징
- **메모리 캐싱**: Map/Object 기반 인메모리 캐싱
- **성능 최적화**: 반복 DB 조회 최소화
- **자동 갱신**: 주기적 캐시 갱신 로직
### 코드 상태
- [x] Prisma import 없음
- [x] query 함수 사용 중
- [x] TypeScript 컴파일 성공
- [x] 캐싱 로직 정상 동작
---
## 📝 비고
이 서비스는 이미 Raw Query로 전환이 완료되어 있어 추가 작업이 필요하지 않습니다.
**상태**: ✅ **완료**
**특이사항**: 캐싱 로직으로 성능에 중요한 서비스

View File

@ -1,92 +0,0 @@
# 📋 Phase 3.18: DDLExecutionService Raw Query 전환 계획
## 📋 개요
DDLExecutionService는 **0개의 Prisma 호출**이 있으며, DDL 실행 및 관리를 담당하는 서비스입니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | -------------------------------------------------- |
| 파일 위치 | `backend-node/src/services/ddlExecutionService.ts` |
| 파일 크기 | 786 라인 |
| Prisma 호출 | 0개 (이미 전환 완료) |
| **현재 진행률** | **6/6 (100%)****전환 완료** |
| 복잡도 | 높음 (DDL 실행, 안전성 검증) |
| 우선순위 | 🔴 높음 (Phase 3.18) |
| **상태** | ✅ **완료** (이미 전환 완료됨) |
---
## ✅ 전환 완료 내역 (이미 완료됨)
DDLExecutionService는 이미 Raw Query로 전환이 완료되었습니다.
### 주요 기능
1. **테이블 생성 (CREATE TABLE)**
- 동적 테이블 생성
- 컬럼 정의 및 제약조건
- 인덱스 생성
2. **컬럼 추가 (ADD COLUMN)**
- 기존 테이블에 컬럼 추가
- 데이터 타입 검증
- 기본값 설정
3. **테이블/컬럼 삭제 (DROP)**
- 안전한 삭제 검증
- 의존성 체크
- 롤백 가능성
4. **DDL 안전성 검증**
- DDL 실행 전 검증
- 순환 참조 방지
- 데이터 손실 방지
5. **DDL 실행 이력**
- 모든 DDL 실행 기록
- 성공/실패 로그
- 롤백 정보
6. **트랜잭션 관리**
- DDL 트랜잭션 처리
- 에러 시 롤백
- 일관성 유지
### 기술적 특징
- **동적 DDL 생성**: 파라미터 기반 DDL 쿼리 생성
- **안전성 검증**: 실행 전 다중 검증 단계
- **감사 로깅**: DDLAuditLogger와 연동
- **PostgreSQL 특화**: PostgreSQL DDL 문법 활용
### 보안 및 안전성
- **SQL 인젝션 방지**: 테이블/컬럼명 화이트리스트 검증
- **권한 검증**: 사용자 권한 확인
- **백업 권장**: DDL 실행 전 백업 체크
- **복구 가능성**: 실행 이력 기록
### 코드 상태
- [x] Prisma import 없음
- [x] query 함수 사용 중
- [x] TypeScript 컴파일 성공
- [x] 안전성 검증 로직 유지
- [x] DDLAuditLogger 연동
---
## 📝 비고
이 서비스는 이미 Raw Query로 전환이 완료되어 있어 추가 작업이 필요하지 않습니다.
**상태**: ✅ **완료**
**특이사항**: DDL 실행의 핵심 서비스로 안전성이 매우 중요
**⚠️ 주의**: 프로덕션 환경에서 DDL 실행 시 각별한 주의 필요

View File

@ -1,369 +0,0 @@
# 🎨 Phase 3.7: LayoutService Raw Query 전환 계획
## 📋 개요
LayoutService는 **10개의 Prisma 호출**이 있으며, 레이아웃 표준 관리를 담당하는 서비스입니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | --------------------------------------------- |
| 파일 위치 | `backend-node/src/services/layoutService.ts` |
| 파일 크기 | 425+ 라인 |
| Prisma 호출 | 10개 |
| **현재 진행률** | **0/10 (0%)** 🔄 **진행 예정** |
| 복잡도 | 중간 (JSON 필드, 검색, 통계) |
| 우선순위 | 🟡 중간 (Phase 3.7) |
| **상태** | ⏳ **대기 중** |
### 🎯 전환 목표
- ⏳ **10개 모든 Prisma 호출을 `db.ts``query()`, `queryOne()` 함수로 교체**
- ⏳ JSON 필드 처리 (layout_config, sections)
- ⏳ 복잡한 검색 조건 처리
- ⏳ GROUP BY 통계 쿼리 전환
- ⏳ 모든 단위 테스트 통과
- ⏳ **Prisma import 완전 제거**
---
## 🔍 Prisma 사용 현황 분석
### 주요 Prisma 호출 (10개)
#### 1. **getLayouts()** - 레이아웃 목록 조회
```typescript
// Line 92, 102
const total = await prisma.layout_standards.count({ where });
const layouts = await prisma.layout_standards.findMany({
where,
skip,
take: size,
orderBy: { updated_date: "desc" },
});
```
#### 2. **getLayoutByCode()** - 레이아웃 단건 조회
```typescript
// Line 152
const layout = await prisma.layout_standards.findFirst({
where: { layout_code: code, company_code: companyCode },
});
```
#### 3. **createLayout()** - 레이아웃 생성
```typescript
// Line 199
const layout = await prisma.layout_standards.create({
data: {
layout_code,
layout_name,
layout_type,
category,
layout_config: safeJSONStringify(layout_config),
sections: safeJSONStringify(sections),
// ... 기타 필드
},
});
```
#### 4. **updateLayout()** - 레이아웃 수정
```typescript
// Line 230, 267
const existing = await prisma.layout_standards.findFirst({
where: { layout_code: code, company_code: companyCode },
});
const updated = await prisma.layout_standards.update({
where: { id: existing.id },
data: { ... },
});
```
#### 5. **deleteLayout()** - 레이아웃 삭제
```typescript
// Line 283, 295
const existing = await prisma.layout_standards.findFirst({
where: { layout_code: code, company_code: companyCode },
});
await prisma.layout_standards.update({
where: { id: existing.id },
data: { is_active: "N", updated_by, updated_date: new Date() },
});
```
#### 6. **getLayoutStatistics()** - 레이아웃 통계
```typescript
// Line 345
const counts = await prisma.layout_standards.groupBy({
by: ["category", "layout_type"],
where: { company_code: companyCode, is_active: "Y" },
_count: { id: true },
});
```
#### 7. **getLayoutCategories()** - 카테고리 목록
```typescript
// Line 373
const existingCodes = await prisma.layout_standards.findMany({
where: { company_code: companyCode },
select: { category: true },
distinct: ["category"],
});
```
---
## 📝 전환 계획
### 1단계: 기본 CRUD 전환 (5개 함수)
**함수 목록**:
- `getLayouts()` - 목록 조회 (count + findMany)
- `getLayoutByCode()` - 단건 조회 (findFirst)
- `createLayout()` - 생성 (create)
- `updateLayout()` - 수정 (findFirst + update)
- `deleteLayout()` - 삭제 (findFirst + update - soft delete)
### 2단계: 통계 및 집계 전환 (2개 함수)
**함수 목록**:
- `getLayoutStatistics()` - 통계 (groupBy)
- `getLayoutCategories()` - 카테고리 목록 (findMany + distinct)
---
## 💻 전환 예시
### 예시 1: 레이아웃 목록 조회 (동적 WHERE + 페이지네이션)
```typescript
// 기존 Prisma
const where: any = { company_code: companyCode };
if (category) where.category = category;
if (layoutType) where.layout_type = layoutType;
if (searchTerm) {
where.OR = [
{ layout_name: { contains: searchTerm, mode: "insensitive" } },
{ layout_code: { contains: searchTerm, mode: "insensitive" } },
];
}
const total = await prisma.layout_standards.count({ where });
const layouts = await prisma.layout_standards.findMany({
where,
skip,
take: size,
orderBy: { updated_date: "desc" },
});
// 전환 후
import { query, queryOne } from "../database/db";
const whereConditions: string[] = ["company_code = $1"];
const values: any[] = [companyCode];
let paramIndex = 2;
if (category) {
whereConditions.push(`category = $${paramIndex++}`);
values.push(category);
}
if (layoutType) {
whereConditions.push(`layout_type = $${paramIndex++}`);
values.push(layoutType);
}
if (searchTerm) {
whereConditions.push(
`(layout_name ILIKE $${paramIndex} OR layout_code ILIKE $${paramIndex})`
);
values.push(`%${searchTerm}%`);
paramIndex++;
}
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
// 총 개수 조회
const countResult = await queryOne<{ count: string }>(
`SELECT COUNT(*) as count FROM layout_standards ${whereClause}`,
values
);
const total = parseInt(countResult?.count || "0");
// 데이터 조회
const layouts = await query<any>(
`SELECT * FROM layout_standards
${whereClause}
ORDER BY updated_date DESC
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
[...values, size, skip]
);
```
### 예시 2: JSON 필드 처리 (레이아웃 생성)
```typescript
// 기존 Prisma
const layout = await prisma.layout_standards.create({
data: {
layout_code,
layout_name,
layout_config: safeJSONStringify(layout_config), // JSON 필드
sections: safeJSONStringify(sections), // JSON 필드
company_code: companyCode,
created_by: createdBy,
},
});
// 전환 후
const layout = await queryOne<any>(
`INSERT INTO layout_standards
(layout_code, layout_name, layout_type, category, layout_config, sections,
company_code, is_active, created_by, updated_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW())
RETURNING *`,
[
layout_code,
layout_name,
layout_type,
category,
safeJSONStringify(layout_config), // JSON 필드는 문자열로 변환
safeJSONStringify(sections),
companyCode,
"Y",
createdBy,
updatedBy,
]
);
```
### 예시 3: GROUP BY 통계 쿼리
```typescript
// 기존 Prisma
const counts = await prisma.layout_standards.groupBy({
by: ["category", "layout_type"],
where: { company_code: companyCode, is_active: "Y" },
_count: { id: true },
});
// 전환 후
const counts = await query<{
category: string;
layout_type: string;
count: string;
}>(
`SELECT category, layout_type, COUNT(*) as count
FROM layout_standards
WHERE company_code = $1 AND is_active = $2
GROUP BY category, layout_type`,
[companyCode, "Y"]
);
// 결과 포맷팅
const formattedCounts = counts.map((row) => ({
category: row.category,
layout_type: row.layout_type,
_count: { id: parseInt(row.count) },
}));
```
### 예시 4: DISTINCT 쿼리 (카테고리 목록)
```typescript
// 기존 Prisma
const existingCodes = await prisma.layout_standards.findMany({
where: { company_code: companyCode },
select: { category: true },
distinct: ["category"],
});
// 전환 후
const existingCodes = await query<{ category: string }>(
`SELECT DISTINCT category
FROM layout_standards
WHERE company_code = $1
ORDER BY category`,
[companyCode]
);
```
---
## ✅ 완료 기준
- [ ] **10개 모든 Prisma 호출을 Raw Query로 전환 완료**
- [ ] **동적 WHERE 조건 생성 (ILIKE, OR)**
- [ ] **JSON 필드 처리 (layout_config, sections)**
- [ ] **GROUP BY 집계 쿼리 전환**
- [ ] **DISTINCT 쿼리 전환**
- [ ] **모든 TypeScript 컴파일 오류 해결**
- [ ] **`import prisma` 완전 제거**
- [ ] **모든 단위 테스트 통과 (10개)**
- [ ] **통합 테스트 작성 완료 (3개 시나리오)**
---
## 🔧 주요 기술적 과제
### 1. JSON 필드 처리
- `layout_config`, `sections` 필드는 JSON 타입
- INSERT/UPDATE 시 `JSON.stringify()` 또는 `safeJSONStringify()` 사용
- SELECT 시 PostgreSQL이 자동으로 JSON 객체로 반환
### 2. 동적 검색 조건
- category, layoutType, searchTerm에 따른 동적 WHERE 절
- OR 조건 처리 (layout_name OR layout_code)
### 3. Soft Delete
- `deleteLayout()`는 실제 삭제가 아닌 `is_active = 'N'` 업데이트
- UPDATE 쿼리 사용
### 4. 통계 쿼리
- `groupBy``GROUP BY` + `COUNT(*)` 전환
- 결과 포맷팅 필요 (`_count.id` 형태로 변환)
---
## 📋 체크리스트
### 코드 전환
- [ ] import 문 수정 (`prisma` → `query, queryOne`)
- [ ] getLayouts() - count + findMany → query + queryOne
- [ ] getLayoutByCode() - findFirst → queryOne
- [ ] createLayout() - create → queryOne (INSERT)
- [ ] updateLayout() - findFirst + update → queryOne (동적 UPDATE)
- [ ] deleteLayout() - findFirst + update → queryOne (UPDATE is_active)
- [ ] getLayoutStatistics() - groupBy → query (GROUP BY)
- [ ] getLayoutCategories() - findMany + distinct → query (DISTINCT)
- [ ] JSON 필드 처리 확인 (safeJSONStringify)
- [ ] Prisma import 완전 제거
### 테스트
- [ ] 단위 테스트 작성 (10개)
- [ ] 통합 테스트 작성 (3개)
- [ ] TypeScript 컴파일 성공
- [ ] 성능 벤치마크 테스트
---
## 💡 특이사항
### JSON 필드 헬퍼 함수
이 서비스는 `safeJSONParse()`, `safeJSONStringify()` 헬퍼 함수를 사용하여 JSON 필드를 안전하게 처리합니다. Raw Query 전환 후에도 이 함수들을 계속 사용해야 합니다.
### Soft Delete 패턴
레이아웃 삭제는 실제 DELETE가 아닌 `is_active = 'N'` 업데이트로 처리되므로, UPDATE 쿼리를 사용해야 합니다.
### 통계 쿼리 결과 포맷
Prisma의 `groupBy``_count: { id: number }` 형태로 반환하지만, Raw Query는 `count: string`으로 반환하므로 포맷팅이 필요합니다.
---
**작성일**: 2025-10-01
**예상 소요 시간**: 1시간
**담당자**: 백엔드 개발팀
**우선순위**: 🟡 중간 (Phase 3.7)
**상태**: ⏳ **대기 중**
**특이사항**: JSON 필드 처리, GROUP BY, DISTINCT 쿼리 포함

View File

@ -1,484 +0,0 @@
# 🗂️ Phase 3.8: DbTypeCategoryService Raw Query 전환 계획
## 📋 개요
DbTypeCategoryService는 **10개의 Prisma 호출**이 있으며, 데이터베이스 타입 카테고리 관리를 담당하는 서비스입니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ------------------------------------------------------ |
| 파일 위치 | `backend-node/src/services/dbTypeCategoryService.ts` |
| 파일 크기 | 320+ 라인 |
| Prisma 호출 | 10개 |
| **현재 진행률** | **0/10 (0%)** 🔄 **진행 예정** |
| 복잡도 | 중간 (CRUD, 통계, UPSERT) |
| 우선순위 | 🟡 중간 (Phase 3.8) |
| **상태** | ⏳ **대기 중** |
### 🎯 전환 목표
- ⏳ **10개 모든 Prisma 호출을 `db.ts``query()`, `queryOne()` 함수로 교체**
- ⏳ ApiResponse 래퍼 패턴 유지
- ⏳ GROUP BY 통계 쿼리 전환
- ⏳ UPSERT 로직 전환 (ON CONFLICT)
- ⏳ 모든 단위 테스트 통과
- ⏳ **Prisma import 완전 제거**
---
## 🔍 Prisma 사용 현황 분석
### 주요 Prisma 호출 (10개)
#### 1. **getAllCategories()** - 카테고리 목록 조회
```typescript
// Line 45
const categories = await prisma.db_type_categories.findMany({
where: { is_active: true },
orderBy: [
{ sort_order: 'asc' },
{ display_name: 'asc' }
]
});
```
#### 2. **getCategoryByTypeCode()** - 카테고리 단건 조회
```typescript
// Line 73
const category = await prisma.db_type_categories.findUnique({
where: { type_code: typeCode }
});
```
#### 3. **createCategory()** - 카테고리 생성
```typescript
// Line 105, 116
const existing = await prisma.db_type_categories.findUnique({
where: { type_code: data.type_code }
});
const category = await prisma.db_type_categories.create({
data: {
type_code: data.type_code,
display_name: data.display_name,
icon: data.icon,
color: data.color,
sort_order: data.sort_order ?? 0,
is_active: true,
}
});
```
#### 4. **updateCategory()** - 카테고리 수정
```typescript
// Line 146
const category = await prisma.db_type_categories.update({
where: { type_code: typeCode },
data: updateData
});
```
#### 5. **deleteCategory()** - 카테고리 삭제 (연결 확인)
```typescript
// Line 179, 193
const connectionsCount = await prisma.external_db_connections.count({
where: { db_type: typeCode }
});
await prisma.db_type_categories.update({
where: { type_code: typeCode },
data: { is_active: false }
});
```
#### 6. **getCategoryStatistics()** - 카테고리별 통계
```typescript
// Line 220, 229
const stats = await prisma.external_db_connections.groupBy({
by: ['db_type'],
_count: { id: true }
});
const categories = await prisma.db_type_categories.findMany({
where: { is_active: true }
});
```
#### 7. **syncPredefinedCategories()** - 사전 정의 카테고리 동기화
```typescript
// Line 300
await prisma.db_type_categories.upsert({
where: { type_code: category.type_code },
update: {
display_name: category.display_name,
icon: category.icon,
color: category.color,
sort_order: category.sort_order,
},
create: {
type_code: category.type_code,
display_name: category.display_name,
icon: category.icon,
color: category.color,
sort_order: category.sort_order,
is_active: true,
},
});
```
---
## 📝 전환 계획
### 1단계: 기본 CRUD 전환 (5개 함수)
**함수 목록**:
- `getAllCategories()` - 목록 조회 (findMany)
- `getCategoryByTypeCode()` - 단건 조회 (findUnique)
- `createCategory()` - 생성 (findUnique + create)
- `updateCategory()` - 수정 (update)
- `deleteCategory()` - 삭제 (count + update - soft delete)
### 2단계: 통계 및 UPSERT 전환 (2개 함수)
**함수 목록**:
- `getCategoryStatistics()` - 통계 (groupBy + findMany)
- `syncPredefinedCategories()` - 동기화 (upsert)
---
## 💻 전환 예시
### 예시 1: 카테고리 목록 조회 (정렬)
```typescript
// 기존 Prisma
const categories = await prisma.db_type_categories.findMany({
where: { is_active: true },
orderBy: [
{ sort_order: 'asc' },
{ display_name: 'asc' }
]
});
// 전환 후
import { query } from "../database/db";
const categories = await query<DbTypeCategory>(
`SELECT * FROM db_type_categories
WHERE is_active = $1
ORDER BY sort_order ASC, display_name ASC`,
[true]
);
```
### 예시 2: 카테고리 생성 (중복 확인)
```typescript
// 기존 Prisma
const existing = await prisma.db_type_categories.findUnique({
where: { type_code: data.type_code }
});
if (existing) {
return {
success: false,
message: "이미 존재하는 타입 코드입니다."
};
}
const category = await prisma.db_type_categories.create({
data: {
type_code: data.type_code,
display_name: data.display_name,
icon: data.icon,
color: data.color,
sort_order: data.sort_order ?? 0,
is_active: true,
}
});
// 전환 후
import { query, queryOne } from "../database/db";
const existing = await queryOne<DbTypeCategory>(
`SELECT * FROM db_type_categories WHERE type_code = $1`,
[data.type_code]
);
if (existing) {
return {
success: false,
message: "이미 존재하는 타입 코드입니다."
};
}
const category = await queryOne<DbTypeCategory>(
`INSERT INTO db_type_categories
(type_code, display_name, icon, color, sort_order, is_active, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
RETURNING *`,
[
data.type_code,
data.display_name,
data.icon || null,
data.color || null,
data.sort_order ?? 0,
true,
]
);
```
### 예시 3: 동적 UPDATE (변경된 필드만)
```typescript
// 기존 Prisma
const updateData: any = {};
if (data.display_name !== undefined) updateData.display_name = data.display_name;
if (data.icon !== undefined) updateData.icon = data.icon;
if (data.color !== undefined) updateData.color = data.color;
if (data.sort_order !== undefined) updateData.sort_order = data.sort_order;
if (data.is_active !== undefined) updateData.is_active = data.is_active;
const category = await prisma.db_type_categories.update({
where: { type_code: typeCode },
data: updateData
});
// 전환 후
const updateFields: string[] = ["updated_at = NOW()"];
const values: any[] = [];
let paramIndex = 1;
if (data.display_name !== undefined) {
updateFields.push(`display_name = $${paramIndex++}`);
values.push(data.display_name);
}
if (data.icon !== undefined) {
updateFields.push(`icon = $${paramIndex++}`);
values.push(data.icon);
}
if (data.color !== undefined) {
updateFields.push(`color = $${paramIndex++}`);
values.push(data.color);
}
if (data.sort_order !== undefined) {
updateFields.push(`sort_order = $${paramIndex++}`);
values.push(data.sort_order);
}
if (data.is_active !== undefined) {
updateFields.push(`is_active = $${paramIndex++}`);
values.push(data.is_active);
}
const category = await queryOne<DbTypeCategory>(
`UPDATE db_type_categories
SET ${updateFields.join(", ")}
WHERE type_code = $${paramIndex}
RETURNING *`,
[...values, typeCode]
);
```
### 예시 4: 삭제 전 연결 확인
```typescript
// 기존 Prisma
const connectionsCount = await prisma.external_db_connections.count({
where: { db_type: typeCode }
});
if (connectionsCount > 0) {
return {
success: false,
message: `이 카테고리를 사용 중인 연결이 ${connectionsCount}개 있습니다.`
};
}
await prisma.db_type_categories.update({
where: { type_code: typeCode },
data: { is_active: false }
});
// 전환 후
const countResult = await queryOne<{ count: string }>(
`SELECT COUNT(*) as count FROM external_db_connections WHERE db_type = $1`,
[typeCode]
);
const connectionsCount = parseInt(countResult?.count || "0");
if (connectionsCount > 0) {
return {
success: false,
message: `이 카테고리를 사용 중인 연결이 ${connectionsCount}개 있습니다.`
};
}
await query(
`UPDATE db_type_categories SET is_active = $1, updated_at = NOW() WHERE type_code = $2`,
[false, typeCode]
);
```
### 예시 5: GROUP BY 통계 + JOIN
```typescript
// 기존 Prisma
const stats = await prisma.external_db_connections.groupBy({
by: ['db_type'],
_count: { id: true }
});
const categories = await prisma.db_type_categories.findMany({
where: { is_active: true }
});
// 전환 후
const stats = await query<{
type_code: string;
display_name: string;
connection_count: string;
}>(
`SELECT
c.type_code,
c.display_name,
COUNT(e.id) as connection_count
FROM db_type_categories c
LEFT JOIN external_db_connections e ON c.type_code = e.db_type
WHERE c.is_active = $1
GROUP BY c.type_code, c.display_name
ORDER BY c.sort_order ASC`,
[true]
);
// 결과 포맷팅
const result = stats.map(row => ({
type_code: row.type_code,
display_name: row.display_name,
connection_count: parseInt(row.connection_count),
}));
```
### 예시 6: UPSERT (ON CONFLICT)
```typescript
// 기존 Prisma
await prisma.db_type_categories.upsert({
where: { type_code: category.type_code },
update: {
display_name: category.display_name,
icon: category.icon,
color: category.color,
sort_order: category.sort_order,
},
create: {
type_code: category.type_code,
display_name: category.display_name,
icon: category.icon,
color: category.color,
sort_order: category.sort_order,
is_active: true,
},
});
// 전환 후
await query(
`INSERT INTO db_type_categories
(type_code, display_name, icon, color, sort_order, is_active, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
ON CONFLICT (type_code)
DO UPDATE SET
display_name = EXCLUDED.display_name,
icon = EXCLUDED.icon,
color = EXCLUDED.color,
sort_order = EXCLUDED.sort_order,
updated_at = NOW()`,
[
category.type_code,
category.display_name,
category.icon || null,
category.color || null,
category.sort_order || 0,
true,
]
);
```
---
## ✅ 완료 기준
- [ ] **10개 모든 Prisma 호출을 Raw Query로 전환 완료**
- [ ] **동적 UPDATE 쿼리 생성**
- [ ] **GROUP BY + LEFT JOIN 통계 쿼리**
- [ ] **ON CONFLICT를 사용한 UPSERT**
- [ ] **ApiResponse 래퍼 패턴 유지**
- [ ] **모든 TypeScript 컴파일 오류 해결**
- [ ] **`import prisma` 완전 제거**
- [ ] **모든 단위 테스트 통과 (10개)**
- [ ] **통합 테스트 작성 완료 (3개 시나리오)**
---
## 🔧 주요 기술적 과제
### 1. ApiResponse 래퍼 패턴
모든 함수가 `ApiResponse<T>` 타입을 반환하므로, 에러 처리를 try-catch로 감싸고 일관된 응답 형식을 유지해야 합니다.
### 2. Soft Delete 패턴
`deleteCategory()`는 실제 DELETE가 아닌 `is_active = false` 업데이트로 처리됩니다.
### 3. 연결 확인
카테고리 삭제 전 `external_db_connections` 테이블에서 사용 중인지 확인해야 합니다.
### 4. UPSERT 로직
PostgreSQL의 `ON CONFLICT` 절을 사용하여 Prisma의 `upsert` 기능을 구현합니다.
### 5. 통계 쿼리 최적화
`groupBy` + 별도 조회 대신, 하나의 `LEFT JOIN` + `GROUP BY` 쿼리로 최적화 가능합니다.
---
## 📋 체크리스트
### 코드 전환
- [ ] import 문 수정 (`prisma` → `query, queryOne`)
- [ ] getAllCategories() - findMany → query
- [ ] getCategoryByTypeCode() - findUnique → queryOne
- [ ] createCategory() - findUnique + create → queryOne (중복 확인 + INSERT)
- [ ] updateCategory() - update → queryOne (동적 UPDATE)
- [ ] deleteCategory() - count + update → queryOne + query
- [ ] getCategoryStatistics() - groupBy + findMany → query (LEFT JOIN)
- [ ] syncPredefinedCategories() - upsert → query (ON CONFLICT)
- [ ] ApiResponse 래퍼 유지
- [ ] Prisma import 완전 제거
### 테스트
- [ ] 단위 테스트 작성 (10개)
- [ ] 통합 테스트 작성 (3개)
- [ ] TypeScript 컴파일 성공
- [ ] 성능 벤치마크 테스트
---
## 💡 특이사항
### ApiResponse 패턴
이 서비스는 모든 메서드가 `ApiResponse<T>` 형식으로 응답을 반환합니다. Raw Query 전환 후에도 이 패턴을 유지해야 합니다.
### 사전 정의 카테고리
`syncPredefinedCategories()` 메서드는 시스템 초기화 시 사전 정의된 DB 타입 카테고리를 동기화합니다. UPSERT 로직이 필수입니다.
### 외래 키 확인
카테고리 삭제 시 `external_db_connections` 테이블에서 사용 중인지 확인하여 데이터 무결성을 보장합니다.
---
**작성일**: 2025-10-01
**예상 소요 시간**: 1시간
**담당자**: 백엔드 개발팀
**우선순위**: 🟡 중간 (Phase 3.8)
**상태**: ⏳ **대기 중**
**특이사항**: ApiResponse 래퍼, UPSERT, GROUP BY + LEFT JOIN 포함

View File

@ -1,408 +0,0 @@
# 📋 Phase 3.9: TemplateStandardService Raw Query 전환 계획
## 📋 개요
TemplateStandardService는 **6개의 Prisma 호출**이 있으며, 템플릿 표준 관리를 담당하는 서비스입니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ------------------------------------------------------ |
| 파일 위치 | `backend-node/src/services/templateStandardService.ts` |
| 파일 크기 | 395 라인 |
| Prisma 호출 | 6개 |
| **현재 진행률** | **7/7 (100%)****전환 완료** |
| 복잡도 | 낮음 (기본 CRUD + DISTINCT) |
| 우선순위 | 🟢 낮음 (Phase 3.9) |
| **상태** | ✅ **완료** |
### 🎯 전환 목표
- ✅ **7개 모든 Prisma 호출을 `db.ts``query()`, `queryOne()` 함수로 교체**
- ✅ 템플릿 CRUD 기능 정상 동작
- ✅ DISTINCT 쿼리 전환
- ✅ Promise.all 병렬 쿼리 (목록 + 개수)
- ✅ 동적 UPDATE 쿼리 (11개 필드)
- ✅ TypeScript 컴파일 성공
- ✅ **Prisma import 완전 제거**
---
## 🔍 Prisma 사용 현황 분석
### 주요 Prisma 호출 (6개)
#### 1. **getTemplateByCode()** - 템플릿 단건 조회
```typescript
// Line 76
return await prisma.template_standards.findUnique({
where: {
template_code: templateCode,
company_code: companyCode,
},
});
```
#### 2. **createTemplate()** - 템플릿 생성
```typescript
// Line 86
const existing = await prisma.template_standards.findUnique({
where: {
template_code: data.template_code,
company_code: data.company_code,
},
});
// Line 96
return await prisma.template_standards.create({
data: {
...data,
created_date: new Date(),
updated_date: new Date(),
},
});
```
#### 3. **updateTemplate()** - 템플릿 수정
```typescript
// Line 164
return await prisma.template_standards.update({
where: {
template_code_company_code: {
template_code: templateCode,
company_code: companyCode,
},
},
data: {
...data,
updated_date: new Date(),
},
});
```
#### 4. **deleteTemplate()** - 템플릿 삭제
```typescript
// Line 181
await prisma.template_standards.delete({
where: {
template_code_company_code: {
template_code: templateCode,
company_code: companyCode,
},
},
});
```
#### 5. **getTemplateCategories()** - 카테고리 목록 (DISTINCT)
```typescript
// Line 262
const categories = await prisma.template_standards.findMany({
where: {
company_code: companyCode,
},
select: {
category: true,
},
distinct: ["category"],
});
```
---
## 📝 전환 계획
### 1단계: 기본 CRUD 전환 (4개 함수)
**함수 목록**:
- `getTemplateByCode()` - 단건 조회 (findUnique)
- `createTemplate()` - 생성 (findUnique + create)
- `updateTemplate()` - 수정 (update)
- `deleteTemplate()` - 삭제 (delete)
### 2단계: 추가 기능 전환 (1개 함수)
**함수 목록**:
- `getTemplateCategories()` - 카테고리 목록 (findMany + distinct)
---
## 💻 전환 예시
### 예시 1: 복합 키 조회
```typescript
// 기존 Prisma
return await prisma.template_standards.findUnique({
where: {
template_code: templateCode,
company_code: companyCode,
},
});
// 전환 후
import { queryOne } from "../database/db";
return await queryOne<any>(
`SELECT * FROM template_standards
WHERE template_code = $1 AND company_code = $2`,
[templateCode, companyCode]
);
```
### 예시 2: 중복 확인 후 생성
```typescript
// 기존 Prisma
const existing = await prisma.template_standards.findUnique({
where: {
template_code: data.template_code,
company_code: data.company_code,
},
});
if (existing) {
throw new Error("이미 존재하는 템플릿 코드입니다.");
}
return await prisma.template_standards.create({
data: {
...data,
created_date: new Date(),
updated_date: new Date(),
},
});
// 전환 후
const existing = await queryOne<any>(
`SELECT * FROM template_standards
WHERE template_code = $1 AND company_code = $2`,
[data.template_code, data.company_code]
);
if (existing) {
throw new Error("이미 존재하는 템플릿 코드입니다.");
}
return await queryOne<any>(
`INSERT INTO template_standards
(template_code, template_name, category, template_type, layout_config,
description, is_active, company_code, created_by, updated_by,
created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW())
RETURNING *`,
[
data.template_code,
data.template_name,
data.category,
data.template_type,
JSON.stringify(data.layout_config),
data.description,
data.is_active,
data.company_code,
data.created_by,
data.updated_by,
]
);
```
### 예시 3: 복합 키 UPDATE
```typescript
// 기존 Prisma
return await prisma.template_standards.update({
where: {
template_code_company_code: {
template_code: templateCode,
company_code: companyCode,
},
},
data: {
...data,
updated_date: new Date(),
},
});
// 전환 후
// 동적 UPDATE 쿼리 생성
const updateFields: string[] = ["updated_date = NOW()"];
const values: any[] = [];
let paramIndex = 1;
if (data.template_name !== undefined) {
updateFields.push(`template_name = $${paramIndex++}`);
values.push(data.template_name);
}
if (data.category !== undefined) {
updateFields.push(`category = $${paramIndex++}`);
values.push(data.category);
}
if (data.template_type !== undefined) {
updateFields.push(`template_type = $${paramIndex++}`);
values.push(data.template_type);
}
if (data.layout_config !== undefined) {
updateFields.push(`layout_config = $${paramIndex++}`);
values.push(JSON.stringify(data.layout_config));
}
if (data.description !== undefined) {
updateFields.push(`description = $${paramIndex++}`);
values.push(data.description);
}
if (data.is_active !== undefined) {
updateFields.push(`is_active = $${paramIndex++}`);
values.push(data.is_active);
}
if (data.updated_by !== undefined) {
updateFields.push(`updated_by = $${paramIndex++}`);
values.push(data.updated_by);
}
return await queryOne<any>(
`UPDATE template_standards
SET ${updateFields.join(", ")}
WHERE template_code = $${paramIndex++} AND company_code = $${paramIndex}
RETURNING *`,
[...values, templateCode, companyCode]
);
```
### 예시 4: 복합 키 DELETE
```typescript
// 기존 Prisma
await prisma.template_standards.delete({
where: {
template_code_company_code: {
template_code: templateCode,
company_code: companyCode,
},
},
});
// 전환 후
import { query } from "../database/db";
await query(
`DELETE FROM template_standards
WHERE template_code = $1 AND company_code = $2`,
[templateCode, companyCode]
);
```
### 예시 5: DISTINCT 쿼리
```typescript
// 기존 Prisma
const categories = await prisma.template_standards.findMany({
where: {
company_code: companyCode,
},
select: {
category: true,
},
distinct: ["category"],
});
return categories
.map((c) => c.category)
.filter((c): c is string => c !== null && c !== undefined)
.sort();
// 전환 후
const categories = await query<{ category: string }>(
`SELECT DISTINCT category
FROM template_standards
WHERE company_code = $1 AND category IS NOT NULL
ORDER BY category ASC`,
[companyCode]
);
return categories.map((c) => c.category);
```
---
## ✅ 완료 기준
- [ ] **6개 모든 Prisma 호출을 Raw Query로 전환 완료**
- [ ] **복합 기본 키 처리 (template_code + company_code)**
- [ ] **동적 UPDATE 쿼리 생성**
- [ ] **DISTINCT 쿼리 전환**
- [ ] **JSON 필드 처리 (layout_config)**
- [ ] **모든 TypeScript 컴파일 오류 해결**
- [ ] **`import prisma` 완전 제거**
- [ ] **모든 단위 테스트 통과 (6개)**
- [ ] **통합 테스트 작성 완료 (2개 시나리오)**
---
## 🔧 주요 기술적 과제
### 1. 복합 기본 키
`template_standards` 테이블은 `(template_code, company_code)` 복합 기본 키를 사용합니다.
- WHERE 절에서 두 컬럼 모두 지정 필요
- Prisma의 `template_code_company_code` 표현식을 `template_code = $1 AND company_code = $2`로 변환
### 2. JSON 필드
`layout_config` 필드는 JSON 타입으로, INSERT/UPDATE 시 `JSON.stringify()` 필요합니다.
### 3. DISTINCT + NULL 제외
카테고리 목록 조회 시 `DISTINCT` 사용하며, NULL 값은 `WHERE category IS NOT NULL`로 제외합니다.
---
## 📋 체크리스트
### 코드 전환
- [ ] import 문 수정 (`prisma` → `query, queryOne`)
- [ ] getTemplateByCode() - findUnique → queryOne (복합 키)
- [ ] createTemplate() - findUnique + create → queryOne (중복 확인 + INSERT)
- [ ] updateTemplate() - update → queryOne (동적 UPDATE, 복합 키)
- [ ] deleteTemplate() - delete → query (복합 키)
- [ ] getTemplateCategories() - findMany + distinct → query (DISTINCT)
- [ ] JSON 필드 처리 (layout_config)
- [ ] Prisma import 완전 제거
### 테스트
- [ ] 단위 테스트 작성 (6개)
- [ ] 통합 테스트 작성 (2개)
- [ ] TypeScript 컴파일 성공
- [ ] 성능 벤치마크 테스트
---
## 💡 특이사항
### 복합 기본 키 패턴
이 서비스는 `(template_code, company_code)` 복합 기본 키를 사용하므로, 모든 조회/수정/삭제 작업에서 두 컬럼을 모두 WHERE 조건에 포함해야 합니다.
### JSON 레이아웃 설정
`layout_config` 필드는 템플릿의 레이아웃 설정을 JSON 형태로 저장합니다. Raw Query 전환 시 `JSON.stringify()`를 사용하여 문자열로 변환해야 합니다.
### 카테고리 관리
템플릿은 카테고리별로 분류되며, `getTemplateCategories()` 메서드로 고유한 카테고리 목록을 조회할 수 있습니다.
---
**작성일**: 2025-10-01
**예상 소요 시간**: 45분
**담당자**: 백엔드 개발팀
**우선순위**: 🟢 낮음 (Phase 3.9)
**상태**: ⏳ **대기 중**
**특이사항**: 복합 기본 키, JSON 필드, DISTINCT 쿼리 포함

View File

@ -1,522 +0,0 @@
# Phase 4.1: AdminController Raw Query 전환 계획
## 📋 개요
관리자 컨트롤러의 Prisma 호출을 Raw Query로 전환합니다.
사용자, 회사, 부서, 메뉴 관리 등 핵심 관리 기능을 포함합니다.
---
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ------------------------------------------------- |
| 파일 위치 | `backend-node/src/controllers/adminController.ts` |
| 파일 크기 | 2,569 라인 |
| Prisma 호출 | 28개 → 0개 |
| **현재 진행률** | **28/28 (100%)****완료** |
| 복잡도 | 중간 (다양한 CRUD 패턴) |
| 우선순위 | 🔴 높음 (Phase 4.1) |
| **상태** | ✅ **완료** (2025-10-01) |
---
## 🔍 Prisma 호출 분석
### 사용자 관리 (13개)
#### 1. getUserList (라인 312-317)
```typescript
const totalCount = await prisma.user_info.count({ where });
const users = await prisma.user_info.findMany({ where, skip, take, orderBy });
```
- **전환**: count → `queryOne`, findMany → `query`
- **복잡도**: 중간 (동적 WHERE, 페이징)
#### 2. getUserInfo (라인 419)
```typescript
const userInfo = await prisma.user_info.findFirst({ where });
```
- **전환**: findFirst → `queryOne`
- **복잡도**: 낮음
#### 3. updateUserStatus (라인 498)
```typescript
await prisma.user_info.update({ where, data });
```
- **전환**: update → `query`
- **복잡도**: 낮음
#### 4. deleteUserByAdmin (라인 2387)
```typescript
await prisma.user_info.update({ where, data: { is_active: "N" } });
```
- **전환**: update (soft delete) → `query`
- **복잡도**: 낮음
#### 5. getMyProfile (라인 1468, 1488, 2479)
```typescript
const user = await prisma.user_info.findUnique({ where });
const dept = await prisma.dept_info.findUnique({ where });
```
- **전환**: findUnique → `queryOne`
- **복잡도**: 낮음
#### 6. updateMyProfile (라인 1864, 2527)
```typescript
const updateResult = await prisma.user_info.update({ where, data });
```
- **전환**: update → `queryOne` with RETURNING
- **복잡도**: 중간 (동적 UPDATE)
#### 7. createOrUpdateUser (라인 1929, 1975)
```typescript
const savedUser = await prisma.user_info.upsert({ where, update, create });
const userCount = await prisma.user_info.count({ where });
```
- **전환**: upsert → `INSERT ... ON CONFLICT`, count → `queryOne`
- **복잡도**: 높음
#### 8. 기타 findUnique (라인 1596, 1832, 2393)
```typescript
const existingUser = await prisma.user_info.findUnique({ where });
const currentUser = await prisma.user_info.findUnique({ where });
const updatedUser = await prisma.user_info.findUnique({ where });
```
- **전환**: findUnique → `queryOne`
- **복잡도**: 낮음
### 회사 관리 (7개)
#### 9. getCompanyList (라인 550, 1276)
```typescript
const companies = await prisma.company_mng.findMany({ orderBy });
```
- **전환**: findMany → `query`
- **복잡도**: 낮음
#### 10. createCompany (라인 2035)
```typescript
const existingCompany = await prisma.company_mng.findFirst({ where });
```
- **전환**: findFirst (중복 체크) → `queryOne`
- **복잡도**: 낮음
#### 11. updateCompany (라인 2172, 2192)
```typescript
const duplicateCompany = await prisma.company_mng.findFirst({ where });
const updatedCompany = await prisma.company_mng.update({ where, data });
```
- **전환**: findFirst → `queryOne`, update → `queryOne`
- **복잡도**: 중간
#### 12. deleteCompany (라인 2261, 2281)
```typescript
const existingCompany = await prisma.company_mng.findUnique({ where });
await prisma.company_mng.delete({ where });
```
- **전환**: findUnique → `queryOne`, delete → `query`
- **복잡도**: 낮음
### 부서 관리 (2개)
#### 13. getDepartmentList (라인 1348)
```typescript
const departments = await prisma.dept_info.findMany({ where, orderBy });
```
- **전환**: findMany → `query`
- **복잡도**: 낮음
#### 14. getDeptInfo (라인 1488)
```typescript
const dept = await prisma.dept_info.findUnique({ where });
```
- **전환**: findUnique → `queryOne`
- **복잡도**: 낮음
### 메뉴 관리 (3개)
#### 15. createMenu (라인 1021)
```typescript
const savedMenu = await prisma.menu_info.create({ data });
```
- **전환**: create → `queryOne` with INSERT RETURNING
- **복잡도**: 중간
#### 16. updateMenu (라인 1087)
```typescript
const updatedMenu = await prisma.menu_info.update({ where, data });
```
- **전환**: update → `queryOne` with UPDATE RETURNING
- **복잡도**: 중간
#### 17. deleteMenu (라인 1149, 1211)
```typescript
const deletedMenu = await prisma.menu_info.delete({ where });
// 재귀 삭제
const deletedMenu = await prisma.menu_info.delete({ where });
```
- **전환**: delete → `query`
- **복잡도**: 중간 (재귀 삭제 로직)
### 다국어 (1개)
#### 18. getMultiLangKeys (라인 665)
```typescript
const result = await prisma.multi_lang_key_master.findMany({ where, orderBy });
```
- **전환**: findMany → `query`
- **복잡도**: 낮음
---
## 📝 전환 전략
### 1단계: Import 변경
```typescript
// 제거
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
// 추가
import { query, queryOne } from "../database/db";
```
### 2단계: 단순 조회 전환
- findMany → `query<T>`
- findUnique/findFirst → `queryOne<T>`
### 3단계: 동적 WHERE 처리
```typescript
const whereConditions: string[] = [];
const params: any[] = [];
let paramIndex = 1;
if (companyCode) {
whereConditions.push(`company_code = $${paramIndex++}`);
params.push(companyCode);
}
const whereClause =
whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : "";
```
### 4단계: 복잡한 로직 전환
- count → `SELECT COUNT(*) as count`
- upsert → `INSERT ... ON CONFLICT DO UPDATE`
- 동적 UPDATE → 조건부 SET 절 생성
### 5단계: 테스트 및 검증
- 각 함수별 동작 확인
- 에러 처리 확인
- 타입 안전성 확인
---
## 🎯 주요 변경 예시
### getUserList (count + findMany)
```typescript
// Before
const totalCount = await prisma.user_info.count({ where });
const users = await prisma.user_info.findMany({
where,
skip,
take,
orderBy,
});
// After
const whereConditions: string[] = [];
const params: any[] = [];
let paramIndex = 1;
// 동적 WHERE 구성
if (where.company_code) {
whereConditions.push(`company_code = $${paramIndex++}`);
params.push(where.company_code);
}
if (where.user_name) {
whereConditions.push(`user_name ILIKE $${paramIndex++}`);
params.push(`%${where.user_name}%`);
}
const whereClause =
whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : "";
// Count
const countResult = await queryOne<{ count: number }>(
`SELECT COUNT(*) as count FROM user_info ${whereClause}`,
params
);
const totalCount = parseInt(countResult?.count?.toString() || "0", 10);
// 데이터 조회
const usersQuery = `
SELECT * FROM user_info
${whereClause}
ORDER BY created_date DESC
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
`;
params.push(take, skip);
const users = await query<UserInfo>(usersQuery, params);
```
### createOrUpdateUser (upsert)
```typescript
// Before
const savedUser = await prisma.user_info.upsert({
where: { user_id: userId },
update: updateData,
create: createData
});
// After
const savedUser = await queryOne<UserInfo>(
`INSERT INTO user_info (user_id, user_name, email, ...)
VALUES ($1, $2, $3, ...)
ON CONFLICT (user_id)
DO UPDATE SET
user_name = EXCLUDED.user_name,
email = EXCLUDED.email,
...
RETURNING *`,
[userId, userName, email, ...]
);
```
### updateMyProfile (동적 UPDATE)
```typescript
// Before
const updateResult = await prisma.user_info.update({
where: { user_id: userId },
data: updateData,
});
// After
const updates: string[] = [];
const params: any[] = [];
let paramIndex = 1;
if (updateData.user_name !== undefined) {
updates.push(`user_name = $${paramIndex++}`);
params.push(updateData.user_name);
}
if (updateData.email !== undefined) {
updates.push(`email = $${paramIndex++}`);
params.push(updateData.email);
}
// ... 다른 필드들
params.push(userId);
const updateResult = await queryOne<UserInfo>(
`UPDATE user_info
SET ${updates.join(", ")}, updated_date = NOW()
WHERE user_id = $${paramIndex}
RETURNING *`,
params
);
```
---
## ✅ 체크리스트
### 기본 설정
- ✅ Prisma import 제거 (완전 제거 확인)
- ✅ query, queryOne import 추가 (이미 존재)
- ✅ 타입 import 확인
### 사용자 관리
- ✅ getUserList (count + findMany → Raw Query)
- ✅ getUserLocale (findFirst → queryOne)
- ✅ setUserLocale (update → query)
- ✅ getUserInfo (findUnique → queryOne)
- ✅ checkDuplicateUserId (findUnique → queryOne)
- ✅ changeUserStatus (findUnique + update → queryOne + query)
- ✅ saveUser (upsert → INSERT ON CONFLICT)
- ✅ updateProfile (동적 update → 동적 query)
- ✅ resetUserPassword (update → query)
### 회사 관리
- ✅ getCompanyList (findMany → query)
- ✅ getCompanyListFromDB (findMany → query)
- ✅ createCompany (findFirst → queryOne)
- ✅ updateCompany (findFirst + update → queryOne + query)
- ✅ deleteCompany (delete → query with RETURNING)
### 부서 관리
- ✅ getDepartmentList (findMany → query with 동적 WHERE)
### 메뉴 관리
- ✅ saveMenu (create → query with INSERT RETURNING)
- ✅ updateMenu (update → query with UPDATE RETURNING)
- ✅ deleteMenu (delete → query with DELETE RETURNING)
- ✅ deleteMenusBatch (다중 delete → 반복 query)
### 다국어
- ✅ getLangKeyList (findMany → query)
### 검증
- ✅ TypeScript 컴파일 확인 (에러 없음)
- ✅ Linter 오류 확인
- ⏳ 기능 테스트 (실행 필요)
- ✅ 에러 처리 확인 (기존 구조 유지)
---
## 📌 참고사항
### 동적 쿼리 생성 패턴
모든 동적 WHERE/UPDATE는 다음 패턴을 따릅니다:
1. 조건/필드 배열 생성
2. 파라미터 배열 생성
3. 파라미터 인덱스 관리
4. SQL 문자열 조합
5. query/queryOne 실행
### 에러 처리
기존 try-catch 구조를 유지하며, 데이터베이스 에러를 적절히 변환합니다.
### 트랜잭션
복잡한 로직은 Service Layer로 이동을 고려합니다.
---
## 🎉 완료 요약 (2025-10-01)
### ✅ 전환 완료 현황
| 카테고리 | 함수 수 | 상태 |
|---------|--------|------|
| 사용자 관리 | 9개 | ✅ 완료 |
| 회사 관리 | 5개 | ✅ 완료 |
| 부서 관리 | 1개 | ✅ 완료 |
| 메뉴 관리 | 4개 | ✅ 완료 |
| 다국어 | 1개 | ✅ 완료 |
| **총계** | **20개** | **✅ 100% 완료** |
### 📊 주요 성과
1. **완전한 Prisma 제거**: adminController.ts에서 모든 Prisma 코드 제거 완료
2. **동적 쿼리 지원**: 런타임 테이블 생성/수정 가능
3. **일관된 에러 처리**: 모든 함수에서 통일된 에러 처리 유지
4. **타입 안전성**: TypeScript 컴파일 에러 없음
5. **코드 품질 향상**: 949줄 변경 (+474/-475)
### 🔑 주요 변환 패턴
#### 1. 동적 WHERE 조건
```typescript
let whereConditions: string[] = [];
let queryParams: any[] = [];
let paramIndex = 1;
if (filter) {
whereConditions.push(`field = $${paramIndex}`);
queryParams.push(filter);
paramIndex++;
}
const whereClause = whereConditions.length > 0
? `WHERE ${whereConditions.join(" AND ")}`
: "";
```
#### 2. UPSERT (INSERT ON CONFLICT)
```typescript
const [result] = await query<any>(
`INSERT INTO table (col1, col2) VALUES ($1, $2)
ON CONFLICT (col1) DO UPDATE SET col2 = $2
RETURNING *`,
[val1, val2]
);
```
#### 3. 동적 UPDATE
```typescript
const updateFields: string[] = [];
const updateValues: any[] = [];
let paramIndex = 1;
if (data.field !== undefined) {
updateFields.push(`field = $${paramIndex}`);
updateValues.push(data.field);
paramIndex++;
}
await query(
`UPDATE table SET ${updateFields.join(", ")} WHERE id = $${paramIndex}`,
[...updateValues, id]
);
```
### 🚀 다음 단계
1. **테스트 실행**: 개발 서버에서 모든 API 엔드포인트 테스트
2. **문서 업데이트**: Phase 4 전체 계획서 진행 상황 반영
3. **다음 Phase**: screenFileController.ts 마이그레이션 진행
---
**마지막 업데이트**: 2025-10-01
**작업자**: Claude Agent
**완료 시간**: 약 15분
**변경 라인 수**: 949줄 (추가 474줄, 삭제 475줄)

View File

@ -1,316 +0,0 @@
# Phase 4: Controller Layer Raw Query 전환 계획
## 📋 개요
컨트롤러 레이어에 남아있는 Prisma 호출을 Raw Query로 전환합니다.
대부분의 컨트롤러는 Service 레이어를 호출하지만, 일부 컨트롤러에서 직접 Prisma를 사용하고 있습니다.
---
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ---------------------------------- |
| 대상 파일 | 7개 컨트롤러 |
| 파일 위치 | `backend-node/src/controllers/` |
| Prisma 호출 | 70개 (28개 완료) |
| **현재 진행률** | **28/70 (40%)** 🔄 **진행 중** |
| 복잡도 | 중간 (대부분 단순 CRUD) |
| 우선순위 | 🟡 중간 (Phase 4) |
| **상태** | 🔄 **진행 중** (adminController 완료) |
---
## 🎯 전환 대상 컨트롤러
### 1. adminController.ts ✅ 완료 (28개)
- **라인 수**: 2,569 라인
- **Prisma 호출**: 28개 → 0개
- **주요 기능**:
- 사용자 관리 (조회, 생성, 수정, 삭제) ✅
- 회사 관리 (조회, 생성, 수정, 삭제) ✅
- 부서 관리 (조회) ✅
- 메뉴 관리 (생성, 수정, 삭제) ✅
- 다국어 키 조회 ✅
- **우선순위**: 🔴 높음
- **상태**: ✅ **완료** (2025-10-01)
- **문서**: [PHASE4.1_ADMIN_CONTROLLER_MIGRATION.md](PHASE4.1_ADMIN_CONTROLLER_MIGRATION.md)
### 2. webTypeStandardController.ts (11개)
- **Prisma 호출**: 11개
- **주요 기능**: 웹타입 표준 관리
- **우선순위**: 🟡 중간
### 3. fileController.ts (11개)
- **Prisma 호출**: 11개
- **주요 기능**: 파일 업로드/다운로드 관리
- **우선순위**: 🟡 중간
### 4. buttonActionStandardController.ts (11개)
- **Prisma 호출**: 11개
- **주요 기능**: 버튼 액션 표준 관리
- **우선순위**: 🟡 중간
### 5. entityReferenceController.ts (4개)
- **Prisma 호출**: 4개
- **주요 기능**: 엔티티 참조 관리
- **우선순위**: 🟢 낮음
### 6. dataflowExecutionController.ts (3개)
- **Prisma 호출**: 3개
- **주요 기능**: 데이터플로우 실행
- **우선순위**: 🟢 낮음
### 7. screenFileController.ts (2개)
- **Prisma 호출**: 2개
- **주요 기능**: 화면 파일 관리
- **우선순위**: 🟢 낮음
---
## 📝 전환 전략
### 기본 원칙
1. **Service Layer 우선**
- 가능하면 Service로 로직 이동
- Controller는 최소한의 로직만 유지
2. **단순 전환**
- 대부분 단순 CRUD → `query`, `queryOne` 사용
- 복잡한 로직은 Service로 이동
3. **에러 처리 유지**
- 기존 try-catch 구조 유지
- 에러 메시지 일관성 유지
### 전환 패턴
#### 1. findMany → query
```typescript
// Before
const users = await prisma.user_info.findMany({
where: { company_code: companyCode },
});
// After
const users = await query<UserInfo>(
`SELECT * FROM user_info WHERE company_code = $1`,
[companyCode]
);
```
#### 2. findUnique → queryOne
```typescript
// Before
const user = await prisma.user_info.findUnique({
where: { user_id: userId },
});
// After
const user = await queryOne<UserInfo>(
`SELECT * FROM user_info WHERE user_id = $1`,
[userId]
);
```
#### 3. create → queryOne with INSERT
```typescript
// Before
const newUser = await prisma.user_info.create({
data: userData
});
// After
const newUser = await queryOne<UserInfo>(
`INSERT INTO user_info (user_id, user_name, ...)
VALUES ($1, $2, ...) RETURNING *`,
[userData.user_id, userData.user_name, ...]
);
```
#### 4. update → queryOne with UPDATE
```typescript
// Before
const updated = await prisma.user_info.update({
where: { user_id: userId },
data: updateData
});
// After
const updated = await queryOne<UserInfo>(
`UPDATE user_info SET user_name = $1, ...
WHERE user_id = $2 RETURNING *`,
[updateData.user_name, ..., userId]
);
```
#### 5. delete → query with DELETE
```typescript
// Before
await prisma.user_info.delete({
where: { user_id: userId },
});
// After
await query(`DELETE FROM user_info WHERE user_id = $1`, [userId]);
```
#### 6. count → queryOne
```typescript
// Before
const count = await prisma.user_info.count({
where: { company_code: companyCode },
});
// After
const result = await queryOne<{ count: number }>(
`SELECT COUNT(*) as count FROM user_info WHERE company_code = $1`,
[companyCode]
);
const count = parseInt(result?.count?.toString() || "0", 10);
```
---
## ✅ 체크리스트
### Phase 4.1: adminController.ts
- [ ] Prisma import 제거
- [ ] query, queryOne import 추가
- [ ] 사용자 관리 함수 전환 (8개)
- [ ] getUserList - count + findMany
- [ ] getUserInfo - findFirst
- [ ] updateUserStatus - update
- [ ] deleteUserByAdmin - update
- [ ] getMyProfile - findUnique
- [ ] updateMyProfile - update
- [ ] createOrUpdateUser - upsert
- [ ] count (getUserList)
- [ ] 회사 관리 함수 전환 (7개)
- [ ] getCompanyList - findMany
- [ ] createCompany - findFirst (중복체크) + create
- [ ] updateCompany - findFirst (중복체크) + update
- [ ] deleteCompany - findUnique + delete
- [ ] 부서 관리 함수 전환 (2개)
- [ ] getDepartmentList - findMany
- [ ] findUnique (부서 조회)
- [ ] 메뉴 관리 함수 전환 (3개)
- [ ] createMenu - create
- [ ] updateMenu - update
- [ ] deleteMenu - delete
- [ ] 기타 함수 전환 (8개)
- [ ] getMultiLangKeys - findMany
- [ ] 컴파일 확인
- [ ] 린터 확인
### Phase 4.2: webTypeStandardController.ts
- [ ] Prisma import 제거
- [ ] query, queryOne import 추가
- [ ] 모든 함수 전환 (11개)
- [ ] 컴파일 확인
- [ ] 린터 확인
### Phase 4.3: fileController.ts
- [ ] Prisma import 제거
- [ ] query, queryOne import 추가
- [ ] 모든 함수 전환 (11개)
- [ ] 컴파일 확인
- [ ] 린터 확인
### Phase 4.4: buttonActionStandardController.ts
- [ ] Prisma import 제거
- [ ] query, queryOne import 추가
- [ ] 모든 함수 전환 (11개)
- [ ] 컴파일 확인
- [ ] 린터 확인
### Phase 4.5: entityReferenceController.ts
- [ ] Prisma import 제거
- [ ] query, queryOne import 추가
- [ ] 모든 함수 전환 (4개)
- [ ] 컴파일 확인
- [ ] 린터 확인
### Phase 4.6: dataflowExecutionController.ts
- [ ] Prisma import 제거
- [ ] query, queryOne import 추가
- [ ] 모든 함수 전환 (3개)
- [ ] 컴파일 확인
- [ ] 린터 확인
### Phase 4.7: screenFileController.ts
- [ ] Prisma import 제거
- [ ] query, queryOne import 추가
- [ ] 모든 함수 전환 (2개)
- [ ] 컴파일 확인
- [ ] 린터 확인
---
## 🎯 예상 결과
### 코드 품질
- ✅ Prisma 의존성 완전 제거
- ✅ 직접적인 SQL 제어
- ✅ 타입 안전성 유지
### 성능
- ✅ 불필요한 ORM 오버헤드 제거
- ✅ 쿼리 최적화 가능
### 유지보수성
- ✅ 명확한 SQL 쿼리
- ✅ 디버깅 용이
- ✅ 데이터베이스 마이그레이션 용이
---
## 📌 참고사항
### Import 변경
```typescript
// Before
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
// After
import { query, queryOne } from "../database/db";
```
### 타입 정의
- 각 테이블의 타입은 `types/` 디렉토리에서 import
- 필요시 새로운 타입 정의 추가
### 에러 처리
- 기존 try-catch 구조 유지
- 적절한 HTTP 상태 코드 반환
- 사용자 친화적 에러 메시지

View File

@ -1,546 +0,0 @@
# Phase 4: 남은 Prisma 호출 전환 계획
## 📊 현재 상황
| 항목 | 내용 |
| --------------- | -------------------------------- |
| 총 Prisma 호출 | 29개 |
| 대상 파일 | 7개 |
| **현재 진행률** | **17/29 (58.6%)** 🔄 **진행 중** |
| 복잡도 | 중간 |
| 우선순위 | 🔴 높음 (Phase 4) |
| **상태** | ⏳ **진행 중** |
---
## 📁 파일별 현황
### ✅ 완료된 파일 (2개)
1. **adminController.ts** - ✅ **28개 완료**
- 사용자 관리: getUserList, getUserInfo, updateUserStatus, deleteUser
- 프로필 관리: getMyProfile, updateMyProfile, resetPassword
- 사용자 생성/수정: createOrUpdateUser (UPSERT)
- 회사 관리: getCompanyList, createCompany, updateCompany, deleteCompany
- 부서 관리: getDepartmentList, getDeptInfo
- 메뉴 관리: createMenu, updateMenu, deleteMenu
- 다국어: getMultiLangKeys, updateLocale
2. **screenFileController.ts** - ✅ **2개 완료**
- getScreenComponentFiles: findMany → query (LIKE)
- getComponentFiles: findMany → query (LIKE)
---
## ⏳ 남은 파일 (5개, 총 12개 호출)
### 1. webTypeStandardController.ts (11개) 🔴 최우선
**위치**: `backend-node/src/controllers/webTypeStandardController.ts`
#### Prisma 호출 목록:
1. **라인 33**: `getWebTypeStandards()` - findMany
```typescript
const webTypes = await prisma.web_type_standards.findMany({
where,
orderBy,
select,
});
```
2. **라인 58**: `getWebTypeStandard()` - findUnique
```typescript
const webTypeData = await prisma.web_type_standards.findUnique({
where: { id },
});
```
3. **라인 112**: `createWebTypeStandard()` - findUnique (중복 체크)
```typescript
const existingWebType = await prisma.web_type_standards.findUnique({
where: { web_type: webType },
});
```
4. **라인 123**: `createWebTypeStandard()` - create
```typescript
const newWebType = await prisma.web_type_standards.create({
data: { ... }
});
```
5. **라인 178**: `updateWebTypeStandard()` - findUnique (존재 확인)
```typescript
const existingWebType = await prisma.web_type_standards.findUnique({
where: { id },
});
```
6. **라인 189**: `updateWebTypeStandard()` - update
```typescript
const updatedWebType = await prisma.web_type_standards.update({
where: { id }, data: { ... }
});
```
7. **라인 230**: `deleteWebTypeStandard()` - findUnique (존재 확인)
```typescript
const existingWebType = await prisma.web_type_standards.findUnique({
where: { id },
});
```
8. **라인 241**: `deleteWebTypeStandard()` - delete
```typescript
await prisma.web_type_standards.delete({
where: { id },
});
```
9. **라인 275**: `updateSortOrder()` - $transaction
```typescript
await prisma.$transaction(
updates.map((item) =>
prisma.web_type_standards.update({ ... })
)
);
```
10. **라인 277**: `updateSortOrder()` - update (트랜잭션 내부)
11. **라인 305**: `getCategories()` - groupBy
```typescript
const categories = await prisma.web_type_standards.groupBy({
by: ["category"],
where,
_count: true,
});
```
**전환 전략**:
- findMany → `query<WebTypeStandard>` with dynamic WHERE
- findUnique → `queryOne<WebTypeStandard>`
- create → `queryOne` with INSERT RETURNING
- update → `queryOne` with UPDATE RETURNING
- delete → `query` with DELETE
- $transaction → `transaction` with client.query
- groupBy → `query` with GROUP BY, COUNT
---
### 2. fileController.ts (1개) 🟡
**위치**: `backend-node/src/controllers/fileController.ts`
#### Prisma 호출:
1. **라인 726**: `downloadFile()` - findUnique
```typescript
const fileRecord = await prisma.attach_file_info.findUnique({
where: { objid: BigInt(objid) },
});
```
**전환 전략**:
- findUnique → `queryOne<AttachFileInfo>`
---
### 3. multiConnectionQueryService.ts (4개) 🟢
**위치**: `backend-node/src/services/multiConnectionQueryService.ts`
#### Prisma 호출 목록:
1. **라인 1005**: `executeSelect()` - $queryRawUnsafe
```typescript
return await prisma.$queryRawUnsafe(query, ...queryParams);
```
2. **라인 1022**: `executeInsert()` - $queryRawUnsafe
```typescript
const insertResult = await prisma.$queryRawUnsafe(...);
```
3. **라인 1055**: `executeUpdate()` - $queryRawUnsafe
```typescript
return await prisma.$queryRawUnsafe(updateQuery, ...updateParams);
```
4. **라인 1071**: `executeDelete()` - $queryRawUnsafe
```typescript
return await prisma.$queryRawUnsafe(...);
```
**전환 전략**:
- $queryRawUnsafe → `query<any>` (이미 Raw SQL 사용 중)
---
### 4. config/database.ts (4개) 🟢
**위치**: `backend-node/src/config/database.ts`
#### Prisma 호출:
1. **라인 1**: PrismaClient import
2. **라인 17**: prisma 인스턴스 생성
3. **라인 22**: `await prisma.$connect()`
4. **라인 31, 35, 40**: `await prisma.$disconnect()`
**전환 전략**:
- 이 파일은 데이터베이스 설정 파일이므로 완전히 제거
- 기존 `db.ts`의 connection pool로 대체
- 모든 import 경로를 `database``database/db`로 변경
---
### 5. routes/ddlRoutes.ts (2개) 🟢
**위치**: `backend-node/src/routes/ddlRoutes.ts`
#### Prisma 호출:
1. **라인 183-184**: 동적 PrismaClient import
```typescript
const { PrismaClient } = await import("@prisma/client");
const prisma = new PrismaClient();
```
2. **라인 186-187**: 연결 테스트
```typescript
await prisma.$queryRaw`SELECT 1`;
await prisma.$disconnect();
```
**전환 전략**:
- 동적 import 제거
- `query('SELECT 1')` 사용
---
### 6. routes/companyManagementRoutes.ts (2개) 🟢
**위치**: `backend-node/src/routes/companyManagementRoutes.ts`
#### Prisma 호출:
1. **라인 32**: findUnique (중복 체크)
```typescript
const existingCompany = await prisma.company_mng.findUnique({
where: { company_code },
});
```
2. **라인 61**: update (회사명 업데이트)
```typescript
await prisma.company_mng.update({
where: { company_code },
data: { company_name },
});
```
**전환 전략**:
- findUnique → `queryOne`
- update → `query`
---
### 7. tests/authService.test.ts (2개) ⚠️
**위치**: `backend-node/src/tests/authService.test.ts`
테스트 파일은 별도 처리 필요 (Phase 5에서 처리)
---
## 🎯 전환 우선순위
### Phase 4.1: 컨트롤러 (완료)
- [x] screenFileController.ts (2개)
- [x] adminController.ts (28개)
### Phase 4.2: 남은 컨트롤러 (진행 예정)
- [ ] webTypeStandardController.ts (11개) - 🔴 최우선
- [ ] fileController.ts (1개)
### Phase 4.3: Routes (진행 예정)
- [ ] ddlRoutes.ts (2개)
- [ ] companyManagementRoutes.ts (2개)
### Phase 4.4: Services (진행 예정)
- [ ] multiConnectionQueryService.ts (4개)
### Phase 4.5: Config (진행 예정)
- [ ] database.ts (4개) - 전체 파일 제거
### Phase 4.6: Tests (Phase 5)
- [ ] authService.test.ts (2개) - 별도 처리
---
## 📋 체크리스트
### webTypeStandardController.ts
- [ ] Prisma import 제거
- [ ] query, queryOne import 추가
- [ ] getWebTypeStandards (findMany → query)
- [ ] getWebTypeStandard (findUnique → queryOne)
- [ ] createWebTypeStandard (findUnique + create → queryOne)
- [ ] updateWebTypeStandard (findUnique + update → queryOne)
- [ ] deleteWebTypeStandard (findUnique + delete → query)
- [ ] updateSortOrder ($transaction → transaction)
- [ ] getCategories (groupBy → query with GROUP BY)
- [ ] TypeScript 컴파일 확인
- [ ] Linter 오류 확인
- [ ] 동작 테스트
### fileController.ts
- [ ] Prisma import 제거
- [ ] queryOne import 추가
- [ ] downloadFile (findUnique → queryOne)
- [ ] TypeScript 컴파일 확인
### routes/ddlRoutes.ts
- [ ] 동적 PrismaClient import 제거
- [ ] query import 추가
- [ ] 연결 테스트 로직 변경
- [ ] TypeScript 컴파일 확인
### routes/companyManagementRoutes.ts
- [ ] Prisma import 제거
- [ ] query, queryOne import 추가
- [ ] findUnique → queryOne
- [ ] update → query
- [ ] TypeScript 컴파일 확인
### services/multiConnectionQueryService.ts
- [ ] Prisma import 제거
- [ ] query import 추가
- [ ] $queryRawUnsafe → query (4곳)
- [ ] TypeScript 컴파일 확인
### config/database.ts
- [ ] 파일 전체 분석
- [ ] 의존성 확인
- [ ] 대체 방안 구현
- [ ] 모든 import 경로 변경
- [ ] 파일 삭제 또는 완전 재작성
---
## 🔧 전환 패턴 요약
### 1. findMany → query
```typescript
// Before
const items = await prisma.table.findMany({ where, orderBy });
// After
const items = await query<T>(
`SELECT * FROM table WHERE ... ORDER BY ...`,
params
);
```
### 2. findUnique → queryOne
```typescript
// Before
const item = await prisma.table.findUnique({ where: { id } });
// After
const item = await queryOne<T>(`SELECT * FROM table WHERE id = $1`, [id]);
```
### 3. create → queryOne with RETURNING
```typescript
// Before
const newItem = await prisma.table.create({ data });
// After
const [newItem] = await query<T>(
`INSERT INTO table (col1, col2) VALUES ($1, $2) RETURNING *`,
[val1, val2]
);
```
### 4. update → query with RETURNING
```typescript
// Before
const updated = await prisma.table.update({ where, data });
// After
const [updated] = await query<T>(
`UPDATE table SET col1 = $1 WHERE id = $2 RETURNING *`,
[val1, id]
);
```
### 5. delete → query
```typescript
// Before
await prisma.table.delete({ where: { id } });
// After
await query(`DELETE FROM table WHERE id = $1`, [id]);
```
### 6. $transaction → transaction
```typescript
// Before
await prisma.$transaction([
prisma.table.update({ ... }),
prisma.table.update({ ... })
]);
// After
await transaction(async (client) => {
await client.query(`UPDATE table SET ...`, params1);
await client.query(`UPDATE table SET ...`, params2);
});
```
### 7. groupBy → query with GROUP BY
```typescript
// Before
const result = await prisma.table.groupBy({
by: ["category"],
_count: true,
});
// After
const result = await query<T>(
`SELECT category, COUNT(*) as count FROM table GROUP BY category`,
[]
);
```
---
## 📈 진행 상황
### 전체 진행률: 17/29 (58.6%)
```
Phase 1-3: Service Layer ████████████████████████████ 100% (415/415)
Phase 4.1: Controllers ████████████████████████████ 100% (30/30)
Phase 4.2: 남은 파일 ███████░░░░░░░░░░░░░░░░░░░░ 58% (17/29)
```
### 상세 진행 상황
| 카테고리 | 완료 | 남음 | 진행률 |
| ----------- | ---- | ---- | ------ |
| Services | 415 | 0 | 100% |
| Controllers | 30 | 11 | 73% |
| Routes | 0 | 4 | 0% |
| Config | 0 | 4 | 0% |
| **총계** | 445 | 19 | 95.9% |
---
## 🎬 다음 단계
1. **webTypeStandardController.ts 전환** (11개)
- 가장 많은 Prisma 호출을 가진 남은 컨트롤러
- 웹 타입 표준 관리 핵심 기능
2. **fileController.ts 전환** (1개)
- 단순 findUnique만 있어 빠르게 처리 가능
3. **Routes 전환** (4개)
- ddlRoutes.ts
- companyManagementRoutes.ts
4. **Service 전환** (4개)
- multiConnectionQueryService.ts
5. **Config 제거** (4개)
- database.ts 완전 제거 또는 재작성
- 모든 의존성 제거
---
## ⚠️ 주의사항
1. **database.ts 처리**
- 현재 많은 파일이 `import prisma from '../config/database'` 사용
- 모든 import를 `import { query, queryOne } from '../database/db'`로 변경 필요
- 단계적으로 진행하여 빌드 오류 방지
2. **BigInt 처리**
- fileController의 `objid: BigInt(objid)``objid::bigint` 또는 `CAST(objid AS BIGINT)`
3. **트랜잭션 처리**
- webTypeStandardController의 `updateSortOrder`는 복잡한 트랜잭션
- `transaction` 함수 사용 필요
4. **타입 안전성**
- 모든 Raw Query에 명시적 타입 지정 필요
- `query<WebTypeStandard>`, `queryOne<AttachFileInfo>`
---
## 📝 완료 후 작업
- [ ] 전체 컴파일 확인
- [ ] Linter 오류 해결
- [ ] 통합 테스트 실행
- [ ] Prisma 관련 의존성 완전 제거 (package.json)
- [ ] `prisma/` 디렉토리 정리
- [ ] 문서 업데이트
- [ ] 커밋 및 Push
---
**작성일**: 2025-10-01
**최종 업데이트**: 2025-10-01
**상태**: 🔄 진행 중 (58.6% 완료)

View File

@ -1,759 +0,0 @@
# 외부 커넥션 관리 REST API 지원 확장 계획서
## 📋 프로젝트 개요
### 목적
현재 외부 데이터베이스 연결만 관리하는 `/admin/external-connections` 페이지에 REST API 연결 관리 기능을 추가하여, DB와 REST API 커넥션을 통합 관리할 수 있도록 확장합니다.
### 현재 상황
- **기존 기능**: 외부 데이터베이스 연결 정보만 관리 (MySQL, PostgreSQL, Oracle, SQL Server, SQLite)
- **기존 테이블**: `external_db_connections` - DB 연결 정보 저장
- **기존 UI**: 단일 화면에서 DB 연결 목록 표시 및 CRUD 작업
### 요구사항
1. **탭 전환**: DB 연결 관리 ↔ REST API 연결 관리 간 탭 전환 UI
2. **REST API 관리**: 요청 주소별 헤더(키-값 쌍) 관리
3. **연결 테스트**: REST API 호출이 정상 작동하는지 테스트 기능
---
## 🗄️ 데이터베이스 설계
### 신규 테이블: `external_rest_api_connections`
```sql
CREATE TABLE external_rest_api_connections (
id SERIAL PRIMARY KEY,
-- 기본 정보
connection_name VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
-- REST API 연결 정보
base_url VARCHAR(500) NOT NULL, -- 기본 URL (예: https://api.example.com)
default_headers JSONB DEFAULT '{}', -- 기본 헤더 정보 (키-값 쌍)
-- 인증 설정
auth_type VARCHAR(20) DEFAULT 'none', -- none, api-key, bearer, basic, oauth2
auth_config JSONB, -- 인증 관련 설정
-- 고급 설정
timeout INTEGER DEFAULT 30000, -- 요청 타임아웃 (ms)
retry_count INTEGER DEFAULT 0, -- 재시도 횟수
retry_delay INTEGER DEFAULT 1000, -- 재시도 간격 (ms)
-- 관리 정보
company_code VARCHAR(20) DEFAULT '*',
is_active CHAR(1) DEFAULT 'Y',
created_date TIMESTAMP DEFAULT NOW(),
created_by VARCHAR(50),
updated_date TIMESTAMP DEFAULT NOW(),
updated_by VARCHAR(50),
-- 테스트 정보
last_test_date TIMESTAMP,
last_test_result CHAR(1), -- Y: 성공, N: 실패
last_test_message TEXT
);
-- 인덱스
CREATE INDEX idx_rest_api_connections_company ON external_rest_api_connections(company_code);
CREATE INDEX idx_rest_api_connections_active ON external_rest_api_connections(is_active);
CREATE INDEX idx_rest_api_connections_name ON external_rest_api_connections(connection_name);
```
### 샘플 데이터
```sql
INSERT INTO external_rest_api_connections (
connection_name, description, base_url, default_headers, auth_type, auth_config
) VALUES
(
'기상청 API',
'기상청 공공데이터 API',
'https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0',
'{"Content-Type": "application/json", "Accept": "application/json"}',
'api-key',
'{"keyLocation": "query", "keyName": "serviceKey", "keyValue": "your-api-key-here"}'
),
(
'사내 인사 시스템 API',
'인사정보 조회용 내부 API',
'https://hr.company.com/api/v1',
'{"Content-Type": "application/json"}',
'bearer',
'{"token": "your-bearer-token-here"}'
);
```
---
## 🔧 백엔드 구현
### 1. 타입 정의
```typescript
// backend-node/src/types/externalRestApiTypes.ts
export type AuthType = "none" | "api-key" | "bearer" | "basic" | "oauth2";
export interface ExternalRestApiConnection {
id?: number;
connection_name: string;
description?: string;
base_url: string;
default_headers: Record<string, string>;
auth_type: AuthType;
auth_config?: {
// API Key
keyLocation?: "header" | "query";
keyName?: string;
keyValue?: string;
// Bearer Token
token?: string;
// Basic Auth
username?: string;
password?: string;
// OAuth2
clientId?: string;
clientSecret?: string;
tokenUrl?: string;
accessToken?: string;
};
timeout?: number;
retry_count?: number;
retry_delay?: number;
company_code: string;
is_active: string;
created_date?: Date;
created_by?: string;
updated_date?: Date;
updated_by?: string;
last_test_date?: Date;
last_test_result?: string;
last_test_message?: string;
}
export interface ExternalRestApiConnectionFilter {
auth_type?: string;
is_active?: string;
company_code?: string;
search?: string;
}
export interface RestApiTestRequest {
id?: number;
base_url: string;
endpoint?: string; // 테스트할 엔드포인트 (선택)
method?: "GET" | "POST" | "PUT" | "DELETE";
headers?: Record<string, string>;
auth_type?: AuthType;
auth_config?: any;
timeout?: number;
}
export interface RestApiTestResult {
success: boolean;
message: string;
response_time?: number;
status_code?: number;
response_data?: any;
error_details?: string;
}
```
### 2. 서비스 계층
```typescript
// backend-node/src/services/externalRestApiConnectionService.ts
export class ExternalRestApiConnectionService {
// CRUD 메서드
static async getConnections(filter: ExternalRestApiConnectionFilter);
static async getConnectionById(id: number);
static async createConnection(data: ExternalRestApiConnection);
static async updateConnection(
id: number,
data: Partial<ExternalRestApiConnection>
);
static async deleteConnection(id: number);
// 테스트 메서드
static async testConnection(
testRequest: RestApiTestRequest
): Promise<RestApiTestResult>;
static async testConnectionById(
id: number,
endpoint?: string
): Promise<RestApiTestResult>;
// 헬퍼 메서드
private static buildHeaders(
connection: ExternalRestApiConnection
): Record<string, string>;
private static validateConnectionData(data: ExternalRestApiConnection): void;
private static encryptSensitiveData(authConfig: any): any;
private static decryptSensitiveData(authConfig: any): any;
}
```
### 3. API 라우트
```typescript
// backend-node/src/routes/externalRestApiConnectionRoutes.ts
// GET /api/external-rest-api-connections - 목록 조회
// GET /api/external-rest-api-connections/:id - 상세 조회
// POST /api/external-rest-api-connections - 새 연결 생성
// PUT /api/external-rest-api-connections/:id - 연결 수정
// DELETE /api/external-rest-api-connections/:id - 연결 삭제
// POST /api/external-rest-api-connections/test - 연결 테스트 (신규)
// POST /api/external-rest-api-connections/:id/test - ID로 테스트 (기존 연결)
```
### 4. 연결 테스트 구현
```typescript
// REST API 연결 테스트 로직
static async testConnection(testRequest: RestApiTestRequest): Promise<RestApiTestResult> {
const startTime = Date.now();
try {
// 헤더 구성
const headers = { ...testRequest.headers };
// 인증 헤더 추가
if (testRequest.auth_type === 'bearer' && testRequest.auth_config?.token) {
headers['Authorization'] = `Bearer ${testRequest.auth_config.token}`;
} else if (testRequest.auth_type === 'basic') {
const credentials = Buffer.from(
`${testRequest.auth_config.username}:${testRequest.auth_config.password}`
).toString('base64');
headers['Authorization'] = `Basic ${credentials}`;
} else if (testRequest.auth_type === 'api-key') {
if (testRequest.auth_config.keyLocation === 'header') {
headers[testRequest.auth_config.keyName] = testRequest.auth_config.keyValue;
}
}
// URL 구성
let url = testRequest.base_url;
if (testRequest.endpoint) {
url = `${testRequest.base_url}${testRequest.endpoint}`;
}
// API Key가 쿼리에 있는 경우
if (testRequest.auth_type === 'api-key' &&
testRequest.auth_config.keyLocation === 'query') {
const separator = url.includes('?') ? '&' : '?';
url = `${url}${separator}${testRequest.auth_config.keyName}=${testRequest.auth_config.keyValue}`;
}
// HTTP 요청 실행
const response = await fetch(url, {
method: testRequest.method || 'GET',
headers,
signal: AbortSignal.timeout(testRequest.timeout || 30000),
});
const responseTime = Date.now() - startTime;
const responseData = await response.json().catch(() => null);
return {
success: response.ok,
message: response.ok ? '연결 성공' : `연결 실패 (${response.status})`,
response_time: responseTime,
status_code: response.status,
response_data: responseData,
};
} catch (error) {
return {
success: false,
message: '연결 실패',
error_details: error instanceof Error ? error.message : '알 수 없는 오류',
};
}
}
```
---
## 🎨 프론트엔드 구현
### 1. 탭 구조 설계
```typescript
// frontend/app/(main)/admin/external-connections/page.tsx
type ConnectionTabType = "database" | "rest-api";
const [activeTab, setActiveTab] = useState<ConnectionTabType>("database");
```
### 2. 메인 페이지 구조 개선
```tsx
// 탭 헤더
<Tabs
value={activeTab}
onValueChange={(value) => setActiveTab(value as ConnectionTabType)}
>
<TabsList className="grid w-[400px] grid-cols-2">
<TabsTrigger value="database" className="flex items-center gap-2">
<Database className="h-4 w-4" />
데이터베이스 연결
</TabsTrigger>
<TabsTrigger value="rest-api" className="flex items-center gap-2">
<Globe className="h-4 w-4" />
REST API 연결
</TabsTrigger>
</TabsList>
{/* 데이터베이스 연결 탭 */}
<TabsContent value="database">
<DatabaseConnectionList />
</TabsContent>
{/* REST API 연결 탭 */}
<TabsContent value="rest-api">
<RestApiConnectionList />
</TabsContent>
</Tabs>
```
### 3. REST API 연결 목록 컴포넌트
```typescript
// frontend/components/admin/RestApiConnectionList.tsx
export function RestApiConnectionList() {
const [connections, setConnections] = useState<ExternalRestApiConnection[]>(
[]
);
const [searchTerm, setSearchTerm] = useState("");
const [authTypeFilter, setAuthTypeFilter] = useState("ALL");
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingConnection, setEditingConnection] = useState<
ExternalRestApiConnection | undefined
>();
// 테이블 컬럼:
// - 연결명
// - 기본 URL
// - 인증 타입
// - 헤더 수 (default_headers 개수)
// - 상태 (활성/비활성)
// - 마지막 테스트 (날짜 + 결과)
// - 작업 (테스트/편집/삭제)
}
```
### 4. REST API 연결 설정 모달
```typescript
// frontend/components/admin/RestApiConnectionModal.tsx
export function RestApiConnectionModal({
isOpen,
onClose,
onSave,
connection,
}: RestApiConnectionModalProps) {
// 섹션 구성:
// 1. 기본 정보
// - 연결명 (필수)
// - 설명
// - 기본 URL (필수)
// 2. 헤더 관리 (키-값 추가/삭제)
// - 동적 입력 필드
// - + 버튼으로 추가
// - 각 행에 삭제 버튼
// 3. 인증 설정
// - 인증 타입 선택 (none/api-key/bearer/basic/oauth2)
// - 선택된 타입별 설정 필드 표시
// 4. 고급 설정 (접기/펼치기)
// - 타임아웃
// - 재시도 설정
// 5. 테스트 섹션
// - 테스트 엔드포인트 입력 (선택)
// - 테스트 실행 버튼
// - 테스트 결과 표시
}
```
### 5. 헤더 관리 컴포넌트
```typescript
// frontend/components/admin/HeadersManager.tsx
interface HeadersManagerProps {
headers: Record<string, string>;
onChange: (headers: Record<string, string>) => void;
}
export function HeadersManager({ headers, onChange }: HeadersManagerProps) {
const [headersList, setHeadersList] = useState<
Array<{ key: string; value: string }>
>(Object.entries(headers).map(([key, value]) => ({ key, value })));
const addHeader = () => {
setHeadersList([...headersList, { key: "", value: "" }]);
};
const removeHeader = (index: number) => {
const newList = headersList.filter((_, i) => i !== index);
setHeadersList(newList);
updateParent(newList);
};
const updateHeader = (
index: number,
field: "key" | "value",
value: string
) => {
const newList = [...headersList];
newList[index][field] = value;
setHeadersList(newList);
updateParent(newList);
};
const updateParent = (list: Array<{ key: string; value: string }>) => {
const headersObject = list.reduce((acc, { key, value }) => {
if (key.trim()) acc[key] = value;
return acc;
}, {} as Record<string, string>);
onChange(headersObject);
};
// UI: 테이블 형태로 키-값 입력 필드 표시
// 각 행: [키 입력] [값 입력] [삭제 버튼]
// 하단: [+ 헤더 추가] 버튼
}
```
### 6. 인증 설정 컴포넌트
```typescript
// frontend/components/admin/AuthenticationConfig.tsx
export function AuthenticationConfig({
authType,
authConfig,
onChange,
}: AuthenticationConfigProps) {
// authType에 따라 다른 입력 필드 표시
// none: 추가 필드 없음
// api-key:
// - 키 위치 (header/query)
// - 키 이름
// - 키 값
// bearer:
// - 토큰 값
// basic:
// - 사용자명
// - 비밀번호
// oauth2:
// - Client ID
// - Client Secret
// - Token URL
// - Access Token (읽기전용, 자동 갱신)
}
```
### 7. API 클라이언트
```typescript
// frontend/lib/api/externalRestApiConnection.ts
export class ExternalRestApiConnectionAPI {
private static readonly BASE_URL = "/api/external-rest-api-connections";
static async getConnections(filter?: ExternalRestApiConnectionFilter) {
const params = new URLSearchParams();
if (filter?.search) params.append("search", filter.search);
if (filter?.auth_type && filter.auth_type !== "ALL") {
params.append("auth_type", filter.auth_type);
}
if (filter?.is_active && filter.is_active !== "ALL") {
params.append("is_active", filter.is_active);
}
const response = await fetch(`${this.BASE_URL}?${params}`);
return this.handleResponse(response);
}
static async getConnectionById(id: number) {
const response = await fetch(`${this.BASE_URL}/${id}`);
return this.handleResponse(response);
}
static async createConnection(data: ExternalRestApiConnection) {
const response = await fetch(this.BASE_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return this.handleResponse(response);
}
static async updateConnection(
id: number,
data: Partial<ExternalRestApiConnection>
) {
const response = await fetch(`${this.BASE_URL}/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return this.handleResponse(response);
}
static async deleteConnection(id: number) {
const response = await fetch(`${this.BASE_URL}/${id}`, {
method: "DELETE",
});
return this.handleResponse(response);
}
static async testConnection(
testRequest: RestApiTestRequest
): Promise<RestApiTestResult> {
const response = await fetch(`${this.BASE_URL}/test`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(testRequest),
});
return this.handleResponse(response);
}
static async testConnectionById(
id: number,
endpoint?: string
): Promise<RestApiTestResult> {
const response = await fetch(`${this.BASE_URL}/${id}/test`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ endpoint }),
});
return this.handleResponse(response);
}
private static async handleResponse(response: Response) {
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || "요청 실패");
}
return response.json();
}
}
```
---
## 📋 구현 순서
### Phase 1: 데이터베이스 및 백엔드 기본 구조 (1일)
- [x] 데이터베이스 테이블 생성 (`external_rest_api_connections`)
- [ ] 타입 정의 작성 (`externalRestApiTypes.ts`)
- [ ] 서비스 계층 기본 CRUD 구현
- [ ] API 라우트 기본 구현
### Phase 2: 연결 테스트 기능 (1일)
- [ ] 연결 테스트 로직 구현
- [ ] 인증 타입별 헤더 구성 로직
- [ ] 에러 처리 및 타임아웃 관리
- [ ] 테스트 결과 저장 (last_test_date, last_test_result)
### Phase 3: 프론트엔드 기본 UI (1-2일)
- [ ] 탭 구조 추가 (Database / REST API)
- [ ] REST API 연결 목록 컴포넌트
- [ ] API 클라이언트 작성
- [ ] 기본 CRUD UI 구현
### Phase 4: 모달 및 상세 기능 (1-2일)
- [ ] REST API 연결 설정 모달
- [ ] 헤더 관리 컴포넌트 (키-값 동적 추가/삭제)
- [ ] 인증 설정 컴포넌트 (타입별 입력 필드)
- [ ] 고급 설정 섹션
### Phase 5: 테스트 및 통합 (1일)
- [ ] 연결 테스트 UI
- [ ] 테스트 결과 표시
- [ ] 에러 처리 및 사용자 피드백
- [ ] 전체 기능 통합 테스트
### Phase 6: 최적화 및 마무리 (0.5일)
- [ ] 민감 정보 암호화 (API 키, 토큰, 비밀번호)
- [ ] UI/UX 개선
- [ ] 문서화
---
## 🧪 테스트 시나리오
### 1. REST API 연결 등록 테스트
- [ ] 기본 정보 입력 (연결명, URL)
- [ ] 헤더 추가/삭제
- [ ] 각 인증 타입별 설정
- [ ] 유효성 검증 (필수 필드, URL 형식)
### 2. 연결 테스트
- [ ] 인증 없는 API 테스트
- [ ] API Key (header/query) 테스트
- [ ] Bearer Token 테스트
- [ ] Basic Auth 테스트
- [ ] 타임아웃 시나리오
- [ ] 네트워크 오류 시나리오
### 3. 데이터 관리
- [ ] 목록 조회 및 필터링
- [ ] 연결 수정
- [ ] 연결 삭제
- [ ] 활성/비활성 전환
### 4. 통합 시나리오
- [ ] DB 연결 탭 ↔ REST API 탭 전환
- [ ] 여러 연결 등록 및 관리
- [ ] 동시 테스트 실행
---
## 🔒 보안 고려사항
### 1. 민감 정보 암호화
```typescript
// API 키, 토큰, 비밀번호 암호화
private static encryptSensitiveData(authConfig: any): any {
if (!authConfig) return null;
const encrypted = { ...authConfig };
// 암호화 대상 필드
if (encrypted.keyValue) {
encrypted.keyValue = encrypt(encrypted.keyValue);
}
if (encrypted.token) {
encrypted.token = encrypt(encrypted.token);
}
if (encrypted.password) {
encrypted.password = encrypt(encrypted.password);
}
if (encrypted.clientSecret) {
encrypted.clientSecret = encrypt(encrypted.clientSecret);
}
return encrypted;
}
```
### 2. 접근 권한 제어
- 관리자 권한만 접근
- 회사별 데이터 분리
- API 호출 시 인증 토큰 검증
### 3. 테스트 요청 제한
- Rate Limiting (1분에 최대 10회)
- 타임아웃 설정 (최대 30초)
- 동시 테스트 제한
---
## 📊 성능 최적화
### 1. 헤더 데이터 구조
```typescript
// JSONB 필드 인덱싱 (PostgreSQL)
CREATE INDEX idx_rest_api_headers ON external_rest_api_connections
USING GIN (default_headers);
CREATE INDEX idx_rest_api_auth_config ON external_rest_api_connections
USING GIN (auth_config);
```
### 2. 캐싱 전략
- 자주 사용되는 연결 정보 캐싱
- 테스트 결과 임시 캐싱 (5분)
---
## 📚 향후 확장 가능성
### 1. 엔드포인트 관리
각 REST API 연결에 대해 자주 사용하는 엔드포인트를 사전 등록하여 빠른 호출 가능
### 2. 요청 템플릿
HTTP 메서드별 요청 바디 템플릿 관리
### 3. 응답 매핑
REST API 응답을 내부 데이터 구조로 변환하는 매핑 룰 설정
### 4. 로그 및 모니터링
- API 호출 이력 기록
- 응답 시간 모니터링
- 오류율 추적
---
## ✅ 완료 체크리스트
### 백엔드
- [ ] 데이터베이스 테이블 생성
- [ ] 타입 정의
- [ ] 서비스 계층 CRUD
- [ ] 연결 테스트 로직
- [ ] API 라우트
- [ ] 민감 정보 암호화
### 프론트엔드
- [ ] 탭 구조
- [ ] REST API 연결 목록
- [ ] 연결 설정 모달
- [ ] 헤더 관리 컴포넌트
- [ ] 인증 설정 컴포넌트
- [ ] API 클라이언트
- [ ] 연결 테스트 UI
### 테스트
- [ ] 단위 테스트
- [ ] 통합 테스트
- [ ] 사용자 시나리오 테스트
### 문서
- [ ] API 문서
- [ ] 사용자 가이드
- [ ] 배포 가이드
---
**작성일**: 2025-10-20
**버전**: 1.0
**담당**: AI Assistant

View File

@ -1,213 +0,0 @@
# REST API 연결 관리 기능 구현 완료
## 구현 개요
외부 커넥션 관리 페이지(`/admin/external-connections`)에 REST API 연결 관리 기능이 추가되었습니다.
기존의 데이터베이스 연결 관리와 함께 REST API 연결도 관리할 수 있도록 탭 기반 UI가 구현되었습니다.
## 구현 완료 사항
### 1. 데이터베이스 (✅ 완료)
**파일**: `/db/create_external_rest_api_connections.sql`
- `external_rest_api_connections` 테이블 생성
- 연결 정보, 인증 설정, 테스트 결과 저장
- JSONB 타입으로 헤더 및 인증 설정 유연하게 관리
- 인덱스 최적화 (company_code, is_active, auth_type, JSONB GIN 인덱스)
**실행 방법**:
```bash
# PostgreSQL 컨테이너에 접속하여 SQL 실행
docker exec -i esgrin-mes-db psql -U postgres -d ilshin < db/create_external_rest_api_connections.sql
```
### 2. 백엔드 구현 (✅ 완료)
#### 2.1 타입 정의
**파일**: `backend-node/src/types/externalRestApiTypes.ts`
- `ExternalRestApiConnection`: REST API 연결 정보 인터페이스
- `RestApiTestRequest`: 연결 테스트 요청 인터페이스
- `RestApiTestResult`: 테스트 결과 인터페이스
- `AuthType`: 인증 타입 (none, api-key, bearer, basic, oauth2)
- 각 인증 타입별 세부 설정 인터페이스
#### 2.2 서비스 레이어
**파일**: `backend-node/src/services/externalRestApiConnectionService.ts`
- CRUD 작업 구현 (생성, 조회, 수정, 삭제)
- 민감 정보 암호화/복호화 (AES-256-GCM)
- REST API 연결 테스트 기능
- 필터링 및 검색 기능
- 유효성 검증
#### 2.3 API 라우트
**파일**: `backend-node/src/routes/externalRestApiConnectionRoutes.ts`
- `GET /api/external-rest-api-connections` - 목록 조회
- `GET /api/external-rest-api-connections/:id` - 상세 조회
- `POST /api/external-rest-api-connections` - 생성
- `PUT /api/external-rest-api-connections/:id` - 수정
- `DELETE /api/external-rest-api-connections/:id` - 삭제
- `POST /api/external-rest-api-connections/test` - 연결 테스트
- `POST /api/external-rest-api-connections/:id/test` - ID 기반 테스트
#### 2.4 앱 통합
**파일**: `backend-node/src/app.ts`
- 새로운 라우트 등록 완료
### 3. 프론트엔드 구현 (✅ 완료)
#### 3.1 API 클라이언트
**파일**: `frontend/lib/api/externalRestApiConnection.ts`
- 백엔드 API와 통신하는 클라이언트 구현
- 타입 안전한 API 호출
- 에러 처리
#### 3.2 공통 컴포넌트
**파일**: `frontend/components/admin/HeadersManager.tsx`
- HTTP 헤더 key-value 관리 컴포넌트
- 동적 추가/삭제 기능
**파일**: `frontend/components/admin/AuthenticationConfig.tsx`
- 인증 타입별 설정 컴포넌트
- 5가지 인증 방식 지원 (none, api-key, bearer, basic, oauth2)
#### 3.3 모달 컴포넌트
**파일**: `frontend/components/admin/RestApiConnectionModal.tsx`
- 연결 추가/수정 모달
- 헤더 관리 및 인증 설정 통합
- 연결 테스트 기능
#### 3.4 목록 관리 컴포넌트
**파일**: `frontend/components/admin/RestApiConnectionList.tsx`
- REST API 연결 목록 표시
- 검색 및 필터링
- CRUD 작업
- 연결 테스트
#### 3.5 메인 페이지
**파일**: `frontend/app/(main)/admin/external-connections/page.tsx`
- 탭 기반 UI 구현 (데이터베이스 ↔ REST API)
- 기존 DB 연결 관리와 통합
## 주요 기능
### 1. 연결 관리
- REST API 연결 정보 생성/수정/삭제
- 연결명, 설명, Base URL 관리
- Timeout, Retry 설정
- 활성화 상태 관리
### 2. 인증 관리
- **None**: 인증 없음
- **API Key**: 헤더 또는 쿼리 파라미터
- **Bearer Token**: Authorization: Bearer {token}
- **Basic Auth**: username/password
- **OAuth2**: client_id, client_secret, token_url 등
### 3. 헤더 관리
- 기본 HTTP 헤더 설정
- Key-Value 형식으로 동적 관리
- Content-Type, Accept 등 자유롭게 설정
### 4. 연결 테스트
- 실시간 연결 테스트
- HTTP 응답 상태 코드 확인
- 응답 시간 측정
- 테스트 결과 저장
### 5. 보안
- 민감 정보 자동 암호화 (AES-256-GCM)
- API Key
- Bearer Token
- 비밀번호
- OAuth2 Client Secret
- 암호화된 데이터는 데이터베이스에 안전하게 저장
## 사용 방법
### 1. SQL 스크립트 실행
```bash
# PostgreSQL 컨테이너에 접속
docker exec -it esgrin-mes-db psql -U postgres -d ilshin
# 또는 파일 직접 실행
docker exec -i esgrin-mes-db psql -U postgres -d ilshin < db/create_external_rest_api_connections.sql
```
### 2. 백엔드 재시작
백엔드 서버가 자동으로 새로운 라우트를 인식합니다. (이미 재시작 완료)
### 3. 웹 UI 접속
1. `/admin/external-connections` 페이지 접속
2. "REST API 연결" 탭 선택
3. "새 연결 추가" 버튼 클릭
4. 필요한 정보 입력
- 연결명, 설명, Base URL
- 기본 헤더 설정
- 인증 타입 선택 및 인증 정보 입력
- Timeout, Retry 설정
5. "연결 테스트" 버튼으로 즉시 테스트 가능
6. 저장
### 4. 연결 관리
- **목록 조회**: 모든 REST API 연결 정보 확인
- **검색**: 연결명, 설명, URL로 검색
- **필터링**: 인증 타입, 활성화 상태로 필터링
- **수정**: 연필 아이콘 클릭하여 수정
- **삭제**: 휴지통 아이콘 클릭하여 삭제
- **테스트**: Play 아이콘 클릭하여 연결 테스트
## 기술 스택
- **Backend**: Node.js, Express, TypeScript, PostgreSQL
- **Frontend**: Next.js, React, TypeScript, Shadcn UI
- **보안**: AES-256-GCM 암호화
- **데이터**: JSONB (PostgreSQL)
## 테스트 완료
- ✅ 백엔드 컴파일 성공
- ✅ 서버 정상 실행 확인
- ✅ 타입 에러 수정 완료
- ✅ 모든 라우트 등록 완료
- ✅ 인증 토큰 자동 포함 구현 (apiClient 사용)
## 다음 단계
1. SQL 스크립트 실행
2. 프론트엔드 빌드 및 테스트
3. UI에서 연결 추가/수정/삭제/테스트 기능 확인
## 참고 문서
- 전체 계획: `PHASE_EXTERNAL_CONNECTION_REST_API_ENHANCEMENT.md`
- 기존 외부 DB 연결: `제어관리_외부커넥션_통합_기능_가이드.md`

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,998 +0,0 @@
# 반응형 레이아웃 시스템 구현 계획서
## 📋 프로젝트 개요
### 목표
화면 디자이너는 절대 위치 기반으로 유지하되, 실제 화면 표시는 반응형으로 동작하도록 전환
### 핵심 원칙
- ✅ 화면 디자이너의 절대 위치 기반 드래그앤드롭은 그대로 유지
- ✅ 실제 화면 표시만 반응형으로 전환
- ✅ 데이터 마이그레이션 불필요 (신규 화면부터 적용)
- ✅ 기존 화면은 불러올 때 스마트 기본값 자동 생성
---
## 🎯 Phase 1: 기본 반응형 시스템 구축 (2-3일)
### 1.1 타입 정의 (2시간)
#### 파일: `frontend/types/responsive.ts`
```typescript
/**
* 브레이크포인트 타입 정의
*/
export type Breakpoint = "desktop" | "tablet" | "mobile";
/**
* 브레이크포인트별 설정
*/
export interface BreakpointConfig {
minWidth: number; // 최소 너비 (px)
maxWidth?: number; // 최대 너비 (px)
columns: number; // 그리드 컬럼 수
}
/**
* 기본 브레이크포인트 설정
*/
export const BREAKPOINTS: Record<Breakpoint, BreakpointConfig> = {
desktop: {
minWidth: 1200,
columns: 12,
},
tablet: {
minWidth: 768,
maxWidth: 1199,
columns: 8,
},
mobile: {
minWidth: 0,
maxWidth: 767,
columns: 4,
},
};
/**
* 브레이크포인트별 반응형 설정
*/
export interface ResponsiveBreakpointConfig {
gridColumns?: number; // 차지할 컬럼 수 (1-12)
order?: number; // 정렬 순서
hide?: boolean; // 숨김 여부
}
/**
* 컴포넌트별 반응형 설정
*/
export interface ResponsiveComponentConfig {
// 기본값 (디자이너에서 설정한 절대 위치)
designerPosition: {
x: number;
y: number;
width: number;
height: number;
};
// 반응형 설정 (선택적)
responsive?: {
desktop?: ResponsiveBreakpointConfig;
tablet?: ResponsiveBreakpointConfig;
mobile?: ResponsiveBreakpointConfig;
};
// 스마트 기본값 사용 여부
useSmartDefaults?: boolean;
}
```
### 1.2 스마트 기본값 생성기 (3시간)
#### 파일: `frontend/lib/utils/responsiveDefaults.ts`
```typescript
import { ComponentData } from "@/types/screen-management";
import { ResponsiveComponentConfig, BREAKPOINTS } from "@/types/responsive";
/**
* 컴포넌트 크기에 따른 스마트 기본값 생성
*
* 로직:
* - 작은 컴포넌트 (너비 25% 이하): 모바일에서도 같은 너비 유지
* - 중간 컴포넌트 (너비 25-50%): 모바일에서 전체 너비로 확장
* - 큰 컴포넌트 (너비 50% 이상): 모든 디바이스에서 전체 너비
*/
export function generateSmartDefaults(
component: ComponentData,
screenWidth: number = 1920
): ResponsiveComponentConfig["responsive"] {
const componentWidthPercent = (component.size.width / screenWidth) * 100;
// 작은 컴포넌트 (25% 이하)
if (componentWidthPercent <= 25) {
return {
desktop: {
gridColumns: 3, // 12컬럼 중 3개 (25%)
order: 1,
hide: false,
},
tablet: {
gridColumns: 2, // 8컬럼 중 2개 (25%)
order: 1,
hide: false,
},
mobile: {
gridColumns: 1, // 4컬럼 중 1개 (25%)
order: 1,
hide: false,
},
};
}
// 중간 컴포넌트 (25-50%)
else if (componentWidthPercent <= 50) {
return {
desktop: {
gridColumns: 6, // 12컬럼 중 6개 (50%)
order: 1,
hide: false,
},
tablet: {
gridColumns: 4, // 8컬럼 중 4개 (50%)
order: 1,
hide: false,
},
mobile: {
gridColumns: 4, // 4컬럼 전체 (100%)
order: 1,
hide: false,
},
};
}
// 큰 컴포넌트 (50% 이상)
else {
return {
desktop: {
gridColumns: 12, // 전체 너비
order: 1,
hide: false,
},
tablet: {
gridColumns: 8, // 전체 너비
order: 1,
hide: false,
},
mobile: {
gridColumns: 4, // 전체 너비
order: 1,
hide: false,
},
};
}
}
/**
* 컴포넌트에 반응형 설정이 없을 경우 자동 생성
*/
export function ensureResponsiveConfig(
component: ComponentData,
screenWidth?: number
): ComponentData {
if (component.responsiveConfig) {
return component;
}
return {
...component,
responsiveConfig: {
designerPosition: {
x: component.position.x,
y: component.position.y,
width: component.size.width,
height: component.size.height,
},
useSmartDefaults: true,
responsive: generateSmartDefaults(component, screenWidth),
},
};
}
```
### 1.3 브레이크포인트 감지 훅 (1시간)
#### 파일: `frontend/hooks/useBreakpoint.ts`
```typescript
import { useState, useEffect } from "react";
import { Breakpoint, BREAKPOINTS } from "@/types/responsive";
/**
* 현재 윈도우 크기에 따른 브레이크포인트 반환
*/
export function useBreakpoint(): Breakpoint {
const [breakpoint, setBreakpoint] = useState<Breakpoint>("desktop");
useEffect(() => {
const updateBreakpoint = () => {
const width = window.innerWidth;
if (width >= BREAKPOINTS.desktop.minWidth) {
setBreakpoint("desktop");
} else if (width >= BREAKPOINTS.tablet.minWidth) {
setBreakpoint("tablet");
} else {
setBreakpoint("mobile");
}
};
// 초기 실행
updateBreakpoint();
// 리사이즈 이벤트 리스너 등록
window.addEventListener("resize", updateBreakpoint);
return () => window.removeEventListener("resize", updateBreakpoint);
}, []);
return breakpoint;
}
/**
* 현재 브레이크포인트의 컬럼 수 반환
*/
export function useGridColumns(): number {
const breakpoint = useBreakpoint();
return BREAKPOINTS[breakpoint].columns;
}
```
### 1.4 반응형 레이아웃 엔진 (6시간)
#### 파일: `frontend/components/screen/ResponsiveLayoutEngine.tsx`
```typescript
import React, { useMemo } from "react";
import { ComponentData } from "@/types/screen-management";
import { Breakpoint, BREAKPOINTS } from "@/types/responsive";
import {
generateSmartDefaults,
ensureResponsiveConfig,
} from "@/lib/utils/responsiveDefaults";
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
interface ResponsiveLayoutEngineProps {
components: ComponentData[];
breakpoint: Breakpoint;
containerWidth: number;
screenWidth?: number;
}
/**
* 반응형 레이아웃 엔진
*
* 절대 위치로 배치된 컴포넌트들을 반응형 그리드로 변환
*
* 변환 로직:
* 1. Y 위치 기준으로 행(row)으로 그룹화
* 2. 각 행 내에서 X 위치 기준으로 정렬
* 3. 반응형 설정 적용 (order, gridColumns, hide)
* 4. CSS Grid로 렌더링
*/
export const ResponsiveLayoutEngine: React.FC<ResponsiveLayoutEngineProps> = ({
components,
breakpoint,
containerWidth,
screenWidth = 1920,
}) => {
// 1단계: 컴포넌트들을 Y 위치 기준으로 행(row)으로 그룹화
const rows = useMemo(() => {
const sortedComponents = [...components].sort(
(a, b) => a.position.y - b.position.y
);
const rows: ComponentData[][] = [];
let currentRow: ComponentData[] = [];
let currentRowY = 0;
const ROW_THRESHOLD = 50; // 같은 행으로 간주할 Y 오차 범위 (px)
sortedComponents.forEach((comp) => {
if (currentRow.length === 0) {
currentRow.push(comp);
currentRowY = comp.position.y;
} else if (Math.abs(comp.position.y - currentRowY) < ROW_THRESHOLD) {
currentRow.push(comp);
} else {
rows.push(currentRow);
currentRow = [comp];
currentRowY = comp.position.y;
}
});
if (currentRow.length > 0) {
rows.push(currentRow);
}
return rows;
}, [components]);
// 2단계: 각 행 내에서 X 위치 기준으로 정렬
const sortedRows = useMemo(() => {
return rows.map((row) =>
[...row].sort((a, b) => a.position.x - b.position.x)
);
}, [rows]);
// 3단계: 반응형 설정 적용
const responsiveComponents = useMemo(() => {
return sortedRows.flatMap((row) =>
row.map((comp) => {
// 반응형 설정이 없으면 자동 생성
const compWithConfig = ensureResponsiveConfig(comp, screenWidth);
// 현재 브레이크포인트의 설정 가져오기
const config = compWithConfig.responsiveConfig!.useSmartDefaults
? generateSmartDefaults(comp, screenWidth)[breakpoint]
: compWithConfig.responsiveConfig!.responsive?.[breakpoint];
return {
...compWithConfig,
responsiveDisplay:
config || generateSmartDefaults(comp, screenWidth)[breakpoint],
};
})
);
}, [sortedRows, breakpoint, screenWidth]);
// 4단계: 필터링 및 정렬
const visibleComponents = useMemo(() => {
return responsiveComponents
.filter((comp) => !comp.responsiveDisplay?.hide)
.sort(
(a, b) =>
(a.responsiveDisplay?.order || 0) - (b.responsiveDisplay?.order || 0)
);
}, [responsiveComponents]);
const gridColumns = BREAKPOINTS[breakpoint].columns;
return (
<div
className="responsive-grid w-full"
style={{
display: "grid",
gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
gap: "16px",
padding: "16px",
}}
>
{visibleComponents.map((comp) => (
<div
key={comp.id}
className="responsive-grid-item"
style={{
gridColumn: `span ${
comp.responsiveDisplay?.gridColumns || gridColumns
}`,
}}
>
<DynamicComponentRenderer component={comp} isPreview={true} />
</div>
))}
</div>
);
};
```
### 1.5 화면 표시 페이지 수정 (4시간)
#### 파일: `frontend/app/(main)/screens/[screenId]/page.tsx`
```typescript
// 기존 import 유지
import { ResponsiveLayoutEngine } from "@/components/screen/ResponsiveLayoutEngine";
import { useBreakpoint } from "@/hooks/useBreakpoint";
export default function ScreenViewPage({
params,
}: {
params: { screenId: string };
}) {
const [layout, setLayout] = useState<LayoutData | null>(null);
const breakpoint = useBreakpoint();
// 반응형 모드 토글 (사용자 설정 또는 화면 설정에 따라)
const [useResponsive, setUseResponsive] = useState(true);
// 기존 로직 유지...
if (!layout) {
return <div>로딩 중...</div>;
}
const screenWidth = layout.screenResolution?.width || 1920;
const screenHeight = layout.screenResolution?.height || 1080;
return (
<div className="h-full w-full bg-white">
{useResponsive ? (
// 반응형 모드
<ResponsiveLayoutEngine
components={layout.components || []}
breakpoint={breakpoint}
containerWidth={window.innerWidth}
screenWidth={screenWidth}
/>
) : (
// 기존 스케일 모드 (하위 호환성)
<div className="overflow-auto" style={{ padding: "16px 0" }}>
<div
style={{
width: `${screenWidth * scale}px`,
minHeight: `${screenHeight * scale}px`,
marginLeft: "16px",
marginRight: "16px",
}}
>
<div
className="relative bg-white"
style={{
width: `${screenWidth}px`,
minHeight: `${screenHeight}px`,
transform: `scale(${scale})`,
transformOrigin: "top left",
}}
>
{layout.components?.map((component) => (
<div
key={component.id}
style={{
position: "absolute",
left: `${component.position.x}px`,
top: `${component.position.y}px`,
width:
component.style?.width || `${component.size.width}px`,
minHeight:
component.style?.height || `${component.size.height}px`,
zIndex: component.position.z || 1,
}}
>
<DynamicComponentRenderer
component={component}
isPreview={true}
/>
</div>
))}
</div>
</div>
</div>
)}
</div>
);
}
```
---
## 🎨 Phase 2: 디자이너 통합 (1-2일)
### 2.1 반응형 설정 패널 (5시간)
#### 파일: `frontend/components/screen/panels/ResponsiveConfigPanel.tsx`
```typescript
import React, { useState } from "react";
import { ComponentData } from "@/types/screen-management";
import {
Breakpoint,
BREAKPOINTS,
ResponsiveComponentConfig,
} from "@/types/responsive";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Checkbox } from "@/components/ui/checkbox";
interface ResponsiveConfigPanelProps {
component: ComponentData;
onUpdate: (config: ResponsiveComponentConfig) => void;
}
export const ResponsiveConfigPanel: React.FC<ResponsiveConfigPanelProps> = ({
component,
onUpdate,
}) => {
const [activeTab, setActiveTab] = useState<Breakpoint>("desktop");
const config = component.responsiveConfig || {
designerPosition: {
x: component.position.x,
y: component.position.y,
width: component.size.width,
height: component.size.height,
},
useSmartDefaults: true,
};
return (
<Card>
<CardHeader>
<CardTitle>반응형 설정</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* 스마트 기본값 토글 */}
<div className="flex items-center space-x-2">
<Checkbox
id="smartDefaults"
checked={config.useSmartDefaults}
onCheckedChange={(checked) => {
onUpdate({
...config,
useSmartDefaults: checked as boolean,
});
}}
/>
<Label htmlFor="smartDefaults">스마트 기본값 사용 (권장)</Label>
</div>
{/* 수동 설정 */}
{!config.useSmartDefaults && (
<Tabs
value={activeTab}
onValueChange={(v) => setActiveTab(v as Breakpoint)}
>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="desktop">데스크톱</TabsTrigger>
<TabsTrigger value="tablet">태블릿</TabsTrigger>
<TabsTrigger value="mobile">모바일</TabsTrigger>
</TabsList>
<TabsContent value={activeTab} className="space-y-4">
{/* 그리드 컬럼 수 */}
<div className="space-y-2">
<Label>너비 (그리드 컬럼)</Label>
<Select
value={config.responsive?.[
activeTab
]?.gridColumns?.toString()}
onValueChange={(v) => {
onUpdate({
...config,
responsive: {
...config.responsive,
[activeTab]: {
...config.responsive?.[activeTab],
gridColumns: parseInt(v),
},
},
});
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="컬럼 수 선택" />
</SelectTrigger>
<SelectContent>
{[...Array(BREAKPOINTS[activeTab].columns)].map((_, i) => {
const cols = i + 1;
const percent = (
(cols / BREAKPOINTS[activeTab].columns) *
100
).toFixed(0);
return (
<SelectItem key={cols} value={cols.toString()}>
{cols} / {BREAKPOINTS[activeTab].columns} ({percent}%)
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
{/* 표시 순서 */}
<div className="space-y-2">
<Label>표시 순서</Label>
<Input
type="number"
min="1"
value={config.responsive?.[activeTab]?.order || 1}
onChange={(e) => {
onUpdate({
...config,
responsive: {
...config.responsive,
[activeTab]: {
...config.responsive?.[activeTab],
order: parseInt(e.target.value),
},
},
});
}}
/>
</div>
{/* 숨김 */}
<div className="flex items-center space-x-2">
<Checkbox
id={`hide-${activeTab}`}
checked={config.responsive?.[activeTab]?.hide || false}
onCheckedChange={(checked) => {
onUpdate({
...config,
responsive: {
...config.responsive,
[activeTab]: {
...config.responsive?.[activeTab],
hide: checked as boolean,
},
},
});
}}
/>
<Label htmlFor={`hide-${activeTab}`}>
{activeTab === "desktop"
? "데스크톱"
: activeTab === "tablet"
? "태블릿"
: "모바일"}
에서 숨김
</Label>
</div>
</TabsContent>
</Tabs>
)}
</CardContent>
</Card>
);
};
```
### 2.2 속성 패널 통합 (1시간)
#### 파일: `frontend/components/screen/panels/UnifiedPropertiesPanel.tsx` 수정
```typescript
// 기존 import에 추가
import { ResponsiveConfigPanel } from './ResponsiveConfigPanel';
// 컴포넌트 내부에 추가
return (
<div className="space-y-4">
{/* 기존 패널들 */}
<PropertiesPanel ... />
<StyleEditor ... />
{/* 반응형 설정 패널 추가 */}
<ResponsiveConfigPanel
component={selectedComponent}
onUpdate={(config) => {
onUpdateComponent({
...selectedComponent,
responsiveConfig: config
});
}}
/>
{/* 기존 세부 설정 패널 */}
<DetailSettingsPanel ... />
</div>
);
```
### 2.3 미리보기 모드 (3시간)
#### 파일: `frontend/components/screen/ScreenDesigner.tsx` 수정
```typescript
// 추가 import
import { Breakpoint } from '@/types/responsive';
import { ResponsiveLayoutEngine } from './ResponsiveLayoutEngine';
import { useBreakpoint } from '@/hooks/useBreakpoint';
import { Button } from '@/components/ui/button';
export const ScreenDesigner: React.FC = () => {
// 미리보기 모드: 'design' | 'desktop' | 'tablet' | 'mobile'
const [previewMode, setPreviewMode] = useState<'design' | Breakpoint>('design');
const currentBreakpoint = useBreakpoint();
// ... 기존 로직 ...
return (
<div className="h-full flex flex-col">
{/* 상단 툴바 */}
<div className="flex gap-2 p-2 border-b bg-white">
<Button
variant={previewMode === 'design' ? 'default' : 'outline'}
size="sm"
onClick={() => setPreviewMode('design')}
>
디자인 모드
</Button>
<Button
variant={previewMode === 'desktop' ? 'default' : 'outline'}
size="sm"
onClick={() => setPreviewMode('desktop')}
>
데스크톱 미리보기
</Button>
<Button
variant={previewMode === 'tablet' ? 'default' : 'outline'}
size="sm"
onClick={() => setPreviewMode('tablet')}
>
태블릿 미리보기
</Button>
<Button
variant={previewMode === 'mobile' ? 'default' : 'outline'}
size="sm"
onClick={() => setPreviewMode('mobile')}
>
모바일 미리보기
</Button>
</div>
{/* 캔버스 영역 */}
<div className="flex-1 overflow-auto">
{previewMode === 'design' ? (
// 기존 절대 위치 기반 디자이너
<Canvas ... />
) : (
// 반응형 미리보기
<div
className="mx-auto border border-gray-300"
style={{
width: previewMode === 'desktop' ? '100%' :
previewMode === 'tablet' ? '768px' :
'375px',
minHeight: '100%'
}}
>
<ResponsiveLayoutEngine
components={components}
breakpoint={previewMode}
containerWidth={
previewMode === 'desktop' ? window.innerWidth :
previewMode === 'tablet' ? 768 :
375
}
screenWidth={selectedScreen?.screenResolution?.width || 1920}
/>
</div>
)}
</div>
</div>
);
};
```
---
## 💾 Phase 3: 저장/불러오기 (1일)
### 3.1 타입 업데이트 (2시간)
#### 파일: `frontend/types/screen-management.ts` 수정
```typescript
import { ResponsiveComponentConfig } from "./responsive";
export interface ComponentData {
// ... 기존 필드들 ...
// 반응형 설정 추가
responsiveConfig?: ResponsiveComponentConfig;
}
```
### 3.2 저장 로직 (2시간)
#### 파일: `frontend/components/screen/ScreenDesigner.tsx` 수정
```typescript
// 저장 함수 수정
const handleSave = async () => {
try {
const layoutData: LayoutData = {
screenResolution: {
width: 1920,
height: 1080,
},
components: components.map((comp) => ({
...comp,
// 반응형 설정이 없으면 자동 생성
responsiveConfig: comp.responsiveConfig || {
designerPosition: {
x: comp.position.x,
y: comp.position.y,
width: comp.size.width,
height: comp.size.height,
},
useSmartDefaults: true,
},
})),
};
await screenApi.updateLayout(selectedScreen.id, layoutData);
// ... 기존 로직 ...
} catch (error) {
console.error("저장 실패:", error);
}
};
```
### 3.3 불러오기 로직 (2시간)
#### 파일: `frontend/components/screen/ScreenDesigner.tsx` 수정
```typescript
import { ensureResponsiveConfig } from "@/lib/utils/responsiveDefaults";
// 화면 불러오기
useEffect(() => {
const loadScreen = async () => {
if (!selectedScreenId) return;
const screen = await screenApi.getScreenById(selectedScreenId);
const layout = await screenApi.getLayout(selectedScreenId);
// 반응형 설정이 없는 컴포넌트에 자동 생성
const componentsWithResponsive = layout.components.map((comp) =>
ensureResponsiveConfig(comp, layout.screenResolution?.width)
);
setSelectedScreen(screen);
setComponents(componentsWithResponsive);
};
loadScreen();
}, [selectedScreenId]);
```
---
## 🧪 Phase 4: 테스트 및 최적화 (1일)
### 4.1 기능 테스트 체크리스트 (3시간)
- [ ] 브레이크포인트 전환 테스트
- [ ] 윈도우 크기 변경 시 자동 전환
- [ ] desktop → tablet → mobile 순차 테스트
- [ ] 스마트 기본값 생성 테스트
- [ ] 작은 컴포넌트 (25% 이하)
- [ ] 중간 컴포넌트 (25-50%)
- [ ] 큰 컴포넌트 (50% 이상)
- [ ] 수동 설정 적용 테스트
- [ ] 그리드 컬럼 변경
- [ ] 표시 순서 변경
- [ ] 디바이스별 숨김
- [ ] 미리보기 모드 테스트
- [ ] 디자인 모드 ↔ 미리보기 모드 전환
- [ ] 각 브레이크포인트 미리보기
- [ ] 저장/불러오기 테스트
- [ ] 반응형 설정 저장
- [ ] 기존 화면 불러오기 시 자동 변환
### 4.2 성능 최적화 (3시간)
#### 레이아웃 계산 메모이제이션
```typescript
// ResponsiveLayoutEngine.tsx
const memoizedLayout = useMemo(() => {
// 레이아웃 계산 로직
}, [components, breakpoint, screenWidth]);
```
#### ResizeObserver 최적화
```typescript
// useBreakpoint.ts
// debounce 적용
const debouncedResize = debounce(updateBreakpoint, 150);
window.addEventListener("resize", debouncedResize);
```
#### 불필요한 리렌더링 방지
```typescript
// React.memo 적용
export const ResponsiveLayoutEngine = React.memo<ResponsiveLayoutEngineProps>(({...}) => {
// ...
});
```
### 4.3 UI/UX 개선 (2시간)
- [ ] 반응형 설정 패널 툴팁 추가
- [ ] 미리보기 모드 전환 애니메이션
- [ ] 로딩 상태 표시
- [ ] 에러 처리 및 사용자 피드백
---
## 📅 최종 타임라인
| Phase | 작업 내용 | 소요 시간 | 누적 시간 |
| ------- | --------------------- | --------- | ------------ |
| Phase 1 | 타입 정의 및 유틸리티 | 6시간 | 6시간 |
| Phase 1 | 반응형 레이아웃 엔진 | 6시간 | 12시간 |
| Phase 1 | 화면 표시 페이지 수정 | 4시간 | 16시간 (2일) |
| Phase 2 | 반응형 설정 패널 | 5시간 | 21시간 |
| Phase 2 | 디자이너 통합 | 4시간 | 25시간 (3일) |
| Phase 3 | 저장/불러오기 | 6시간 | 31시간 (4일) |
| Phase 4 | 테스트 및 최적화 | 8시간 | 39시간 (5일) |
**총 예상 시간: 39시간 (약 5일)**
---
## 🎯 구현 우선순위
### 1단계: 핵심 기능 (필수)
1. ✅ 타입 정의
2. ✅ 스마트 기본값 생성기
3. ✅ 브레이크포인트 훅
4. ✅ 반응형 레이아웃 엔진
5. ✅ 화면 표시 페이지 수정
### 2단계: 디자이너 UI (중요)
6. ✅ 반응형 설정 패널
7. ✅ 속성 패널 통합
8. ✅ 미리보기 모드
### 3단계: 데이터 처리 (중요)
9. ✅ 타입 업데이트
10. ✅ 저장/불러오기 로직
### 4단계: 완성도 (선택)
11. 테스트
12. 최적화
13. UI/UX 개선
---
## ✅ 완료 체크리스트
### Phase 1: 기본 시스템
- [ ] `frontend/types/responsive.ts` 생성
- [ ] `frontend/lib/utils/responsiveDefaults.ts` 생성
- [ ] `frontend/hooks/useBreakpoint.ts` 생성
- [ ] `frontend/components/screen/ResponsiveLayoutEngine.tsx` 생성
- [ ] `frontend/app/(main)/screens/[screenId]/page.tsx` 수정
### Phase 2: 디자이너 통합
- [ ] `frontend/components/screen/panels/ResponsiveConfigPanel.tsx` 생성
- [ ] `frontend/components/screen/panels/UnifiedPropertiesPanel.tsx` 수정
- [ ] `frontend/components/screen/ScreenDesigner.tsx` 수정
### Phase 3: 데이터 처리
- [ ] `frontend/types/screen-management.ts` 수정
- [ ] 저장 로직 수정
- [ ] 불러오기 로직 수정
### Phase 4: 테스트
- [ ] 기능 테스트 완료
- [ ] 성능 최적화 완료
- [ ] UI/UX 개선 완료
---
## 🚀 시작 준비 완료
이제 Phase 1부터 순차적으로 구현을 시작합니다.

104
PLAN.MD Normal file
View File

@ -0,0 +1,104 @@
# 프로젝트: 화면 관리 기능 개선 (복제/삭제/그룹 관리/테이블 설정)
## 개요
화면 관리 시스템의 복제, 삭제, 수정, 테이블 설정 기능을 전면 개선하여 효율적인 화면 관리를 지원합니다.
## 핵심 기능
### 1. 단일 화면 복제
- [x] 우클릭 컨텍스트 메뉴에서 "복제" 선택
- [x] 화면명, 화면 코드 자동 생성 (중복 시 `_COPY` 접미사 추가)
- [x] 연결된 모달 화면 함께 복제
- [x] 대상 그룹 선택 가능
- [x] 복제 후 목록 자동 새로고침
### 2. 그룹(폴더) 전체 복제
- [x] 대분류 폴더 복제 시 모든 하위 폴더 + 화면 재귀적 복제
- [x] 정렬 순서(display_order) 유지
- [x] 대분류(최상위 그룹) 복제 시 경고 문구 표시
- [x] 정렬 순서 입력 필드 추가
- [x] 복제 모드 선택: 전체(폴더+화면), 폴더만, 화면만
- [x] 모달 스크롤 지원 (max-h-[90vh] overflow-y-auto)
### 3. 고급 옵션: 이름 일괄 변경
- [x] 찾을 텍스트 / 대체할 텍스트 (Find & Replace)
- [x] 미리보기 기능
### 4. 삭제 기능
- [x] 단일 화면 삭제 (휴지통으로 이동)
- [x] 그룹 삭제 (화면 함께 삭제 옵션)
- [x] 삭제 시 로딩 프로그레스 바 표시
### 5. 화면 수정 기능
- [x] 우클릭 "수정" 메뉴로 화면 이름/그룹/역할/정렬 순서 변경
- [x] 그룹 추가/수정 시 상위 그룹 기반 자동 회사 코드 설정
### 6. 테이블 설정 기능 (TableSettingModal)
- [x] 화면 설정 모달에 "테이블 설정" 탭 추가
- [x] 입력 타입 변경 시 관련 참조 필드 자동 초기화
- 엔티티→텍스트: referenceTable, referenceColumn, displayColumn 초기화
- 코드→다른 타입: codeCategory, codeValue 초기화
- [x] 데이터 일관성 유지 (inputType ↔ referenceTable 연동)
- [x] 조인 배지 단일화 (FK 배지 제거, 조인 배지만 표시)
### 7. 회사 코드 지원 (최고 관리자)
- [x] 대상 회사 선택 가능
- [x] 상위 그룹 선택 시 자동 회사 코드 설정
## 관련 파일
- `frontend/components/screen/CopyScreenModal.tsx` - 복제 모달
- `frontend/components/screen/ScreenGroupTreeView.tsx` - 트리 뷰 + 컨텍스트 메뉴
- `frontend/components/screen/TableSettingModal.tsx` - 테이블 설정 모달
- `frontend/components/screen/ScreenSettingModal.tsx` - 화면 설정 모달 (테이블 설정 탭 포함)
- `frontend/lib/api/screen.ts` - 화면 API
- `frontend/lib/api/screenGroup.ts` - 그룹 API
- `frontend/lib/api/tableManagement.ts` - 테이블 관리 API
## 진행 상태
- [완료] 단일 화면 복제 + 새로고침
- [완료] 그룹 전체 복제 (재귀적)
- [완료] 고급 옵션: 이름 일괄 변경 (Find & Replace)
- [완료] 단일 화면/그룹 삭제 + 로딩 프로그레스
- [완료] 화면 수정 (이름/그룹/역할/순서)
- [완료] 테이블 설정 탭 추가
- [완료] 입력 타입 변경 시 관련 필드 초기화
- [완료] 그룹 복제 모달 스크롤 문제 수정
---
# 이전 프로젝트: 외부 REST API 커넥션 관리 확장 (POST/Body 지원)
## 개요
현재 GET 방식 위주로 구현된 외부 REST API 커넥션 관리 기능을 확장하여, POST, PUT, DELETE 등 다양한 HTTP 메서드와 JSON Request Body를 설정하고 테스트할 수 있도록 개선합니다. 이를 통해 토큰 발급 API나 데이터 전송 API 등 다양한 외부 시스템과의 연동을 지원합니다.
## 핵심 기능
1. **DB 스키마 확장**: `external_rest_api_connections` 테이블에 `default_method`, `default_body` 컬럼 추가
2. **백엔드 로직 개선**:
- 커넥션 생성/수정 시 메서드와 바디 정보 저장
- 연결 테스트 시 설정된 메서드와 바디를 사용하여 요청 수행
- SSL 인증서 검증 우회 옵션 적용 (내부망/테스트망 지원)
3. **프론트엔드 UI 개선**:
- 커넥션 설정 모달에 HTTP 메서드 선택(Select) 및 Body 입력(Textarea/JSON Editor) 필드 추가
- 테스트 기능에서 Body 데이터 포함하여 요청 전송
## 테스트 계획
### 1단계: 기본 기능 및 DB 마이그레이션
- [x] DB 마이그레이션 스크립트 작성 및 실행
- [x] 백엔드 타입 정의 수정 (`default_method`, `default_body` 추가)
### 2단계: 백엔드 로직 구현
- [x] 커넥션 생성/수정 API 수정 (필드 추가)
- [x] 커넥션 상세 조회 API 확인
- [x] 연결 테스트 API 수정 (Method, Body 반영하여 요청 전송)
### 3단계: 프론트엔드 구현
- [x] 커넥션 관리 리스트/모달 UI 수정
- [x] 연결 테스트 UI 수정 및 기능 확인
## 에러 처리 계획
- **JSON 파싱 에러**: Body 입력값이 유효한 JSON이 아닐 경우 에러 처리
- **API 호출 에러**: 외부 API 호출 실패 시 상세 로그 기록 및 클라이언트에 에러 메시지 전달
- **SSL 인증 에러**: `rejectUnauthorized: false` 옵션으로 처리 (기존 `RestApiConnector` 활용)
## 진행 상태
- [완료] 모든 단계 구현 완료

680
PLAN_RENEWAL.md Normal file
View File

@ -0,0 +1,680 @@
# Screen Designer 2.0 리뉴얼 계획: 컴포넌트 통합 및 속성 기반 고도화
## 1. 개요
현재 **68개 이상**으로 파편화된 화면 관리 컴포넌트들을 **9개의 핵심 통합 컴포넌트(Unified Components)**로 재편합니다.
각 컴포넌트는 **속성(Config)** 설정을 통해 다양한 형태(View Mode)와 기능(Behavior)을 수행하도록 설계되어, 유지보수성과 확장성을 극대화합니다.
### 현재 컴포넌트 현황 (AS-IS)
| 카테고리 | 파일 수 | 주요 파일들 |
| :------------- | :-----: | :------------------------------------------------------------------ |
| Widget 타입별 | 14개 | TextWidget, NumberWidget, SelectWidget, DateWidget, EntityWidget 등 |
| Config Panel | 28개 | TextConfigPanel, SelectConfigPanel, DateConfigPanel 등 |
| WebType Config | 11개 | TextTypeConfigPanel, SelectTypeConfigPanel 등 |
| 기타 패널 | 15개+ | PropertiesPanel, DetailSettingsPanel 등 |
---
## 2. 통합 전략: 9 Core Widgets
### A. 입력 위젯 (Input Widgets) - 5종
단순 데이터 입력 필드를 통합합니다.
| 통합 컴포넌트 (TO-BE) | 포함되는 기존 컴포넌트 (AS-IS) | 핵심 속성 (Configuration) |
| :-------------------- | :------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------- |
| **1. Unified Select** | Select, Radio, Checkbox, Boolean, Code, Entity, Combobox, Toggle | **`mode`**: "dropdown" / "radio" / "check" / "tag"<br>**`source`**: "static" / "code" / "db" / "api"<br>**`dependency`**: { parentField: "..." } |
| **2. Unified Input** | Text, Number, Email, Tel, Password, Color, Search, Integer, Decimal | **`type`**: "text" / "number" / "password"<br>**`format`**: "email", "currency", "biz_no"<br>**`mask`**: "000-0000-0000" |
| **3. Unified Date** | Date, Time, DateTime, DateRange, Month, Year | **`type`**: "date" / "time" / "datetime"<br>**`range`**: true/false |
| **4. Unified Text** | Textarea, RichEditor, Markdown, HTML | **`mode`**: "simple" / "rich" / "code"<br>**`rows`**: number |
| **5. Unified Media** | File, Image, Video, Audio, Attachment | **`type`**: "file" / "image"<br>**`multiple`**: true/false<br>**`preview`**: true/false |
### B. 구조/데이터 위젯 (Structure & Data Widgets) - 4종
레이아웃 배치와 데이터 시각화를 담당합니다.
| 통합 컴포넌트 (TO-BE) | 포함되는 기존 컴포넌트 (AS-IS) | 핵심 속성 (Configuration) | 활용 예시 |
| :-------------------- | :-------------------------------------------------- | :------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------- |
| **6. Unified List** | **Table, Card List, Repeater, DataGrid, List View** | **`viewMode`**: "table" / "card" / "kanban"<br>**`editable`**: true/false | - `viewMode='table'`: 엑셀형 리스트<br>- `viewMode='card'`: **카드 디스플레이**<br>- `editable=true`: **반복 필드 그룹** |
| **7. Unified Layout** | **Row, Col, Split Panel, Grid, Spacer** | **`type`**: "grid" / "split" / "flex"<br>**`columns`**: number | - `type='split'`: **화면 분할 패널**<br>- `type='grid'`: 격자 레이아웃 |
| **8. Unified Group** | Tab, Accordion, FieldSet, Modal, Section | **`type`**: "tab" / "accordion" / "modal" | - 탭이나 아코디언으로 내용 그룹화 |
| **9. Unified Biz** | **Rack Structure**, Calendar, Gantt | **`type`**: "rack" / "calendar" / "gantt" | - `type='rack'`: **랙 구조 설정**<br>- 특수 비즈니스 로직 플러그인 탑재 |
### C. Config Panel 통합 전략 (핵심)
현재 28개의 ConfigPanel을 **1개의 DynamicConfigPanel**로 통합합니다.
| AS-IS | TO-BE | 방식 |
| :-------------------- | :--------------------- | :------------------------------- |
| TextConfigPanel.tsx | | |
| SelectConfigPanel.tsx | **DynamicConfigPanel** | DB의 `sys_input_type` 테이블에서 |
| DateConfigPanel.tsx | (단일 컴포넌트) | JSON Schema를 읽어 |
| NumberConfigPanel.tsx | | 속성 UI를 동적 생성 |
| ... 24개 더 | | |
---
## 3. 구현 시나리오 (속성 기반 변신)
### Case 1: "테이블을 카드 리스트로 변경"
- **AS-IS**: `DataTable` 컴포넌트를 삭제하고 `CardList` 컴포넌트를 새로 추가해야 함.
- **TO-BE**: `UnifiedList`의 속성창에서 **[View Mode]**를 `Table``Card`로 변경하면 즉시 반영.
### Case 2: "단일 선택을 라디오 버튼으로 변경"
- **AS-IS**: `SelectWidget`을 삭제하고 `RadioWidget` 추가.
- **TO-BE**: `UnifiedSelect` 속성창에서 **[Display Mode]**를 `Dropdown``Radio`로 변경.
### Case 3: "입력 폼에 반복 필드(Repeater) 추가"
- **TO-BE**: `UnifiedList` 컴포넌트 배치 후 `editable: true`, `viewMode: "table"` 설정.
---
## 4. 실행 로드맵 (Action Plan)
### Phase 0: 준비 단계 (1주)
통합 작업 전 필수 분석 및 설계를 진행합니다.
- [ ] 기존 컴포넌트 사용 현황 분석 (화면별 위젯 사용 빈도 조사)
- [ ] 데이터 마이그레이션 전략 설계 (`widgetType` → `UnifiedWidget.type` 매핑 정의)
- [ ] `sys_input_type` 테이블 JSON Schema 설계
- [ ] DynamicConfigPanel 프로토타입 설계
### Phase 1: 입력 위젯 통합 (2주)
가장 중복이 많고 효과가 즉각적인 입력 필드부터 통합합니다.
- [ ] **UnifiedInput 구현**: Text, Number, Email, Tel, Password 통합
- [ ] **UnifiedSelect 구현**: Select, Radio, Checkbox, Boolean 통합
- [ ] **UnifiedDate 구현**: Date, DateTime, Time 통합
- [ ] 기존 위젯과 **병행 운영** (deprecated 마킹, 삭제하지 않음)
### Phase 2: Config Panel 통합 (2주)
28개의 ConfigPanel을 단일 동적 패널로 통합합니다.
- [ ] **DynamicConfigPanel 구현**: DB 스키마 기반 속성 UI 자동 생성
- [ ] `sys_input_type` 테이블에 위젯별 JSON Schema 정의 저장
- [ ] 기존 ConfigPanel과 **병행 운영** (삭제하지 않음)
### Phase 3: 데이터/레이아웃 위젯 통합 (2주)
프로젝트의 데이터를 보여주는 핵심 뷰를 통합합니다.
- [ ] **UnifiedList 구현**: Table, Card, Repeater 통합 렌더러 개발
- [ ] **UnifiedLayout 구현**: Split Panel, Grid, Flex 통합
- [ ] **UnifiedGroup 구현**: Tab, Accordion, Modal 통합
### Phase 4: 안정화 및 마이그레이션 (2주)
신규 컴포넌트 안정화 후 점진적 전환을 진행합니다.
- [ ] 신규 화면은 Unified 컴포넌트만 사용하도록 가이드
- [ ] 기존 화면 데이터 마이그레이션 스크립트 개발
- [ ] 마이그레이션 테스트 (스테이징 환경)
- [ ] 문서화 및 개발 가이드 작성
### Phase 5: 레거시 정리 (추후 결정)
충분한 안정화 기간 후 레거시 컴포넌트 정리를 검토합니다.
- [ ] 사용 현황 재분석 (Unified 전환율 확인)
- [ ] 미전환 화면 목록 정리
- [ ] 레거시 컴포넌트 삭제 여부 결정 (별도 회의)
---
## 5. 데이터 마이그레이션 전략
### 5.1 위젯 타입 매핑 테이블
기존 `widgetType`을 신규 Unified 컴포넌트로 매핑합니다.
| 기존 widgetType | 신규 컴포넌트 | 속성 설정 |
| :-------------- | :------------ | :------------------------------ |
| `text` | UnifiedInput | `type: "text"` |
| `number` | UnifiedInput | `type: "number"` |
| `email` | UnifiedInput | `type: "text", format: "email"` |
| `tel` | UnifiedInput | `type: "text", format: "tel"` |
| `select` | UnifiedSelect | `mode: "dropdown"` |
| `radio` | UnifiedSelect | `mode: "radio"` |
| `checkbox` | UnifiedSelect | `mode: "check"` |
| `date` | UnifiedDate | `type: "date"` |
| `datetime` | UnifiedDate | `type: "datetime"` |
| `textarea` | UnifiedText | `mode: "simple"` |
| `file` | UnifiedMedia | `type: "file"` |
| `image` | UnifiedMedia | `type: "image"` |
### 5.2 마이그레이션 원칙
1. **비파괴적 전환**: 기존 데이터 구조 유지, 신규 필드 추가 방식
2. **하위 호환성**: 기존 `widgetType` 필드는 유지, `unifiedType` 필드 추가
3. **점진적 전환**: 화면 수정 시점에 자동 또는 수동 전환
---
## 6. 기대 효과
1. **컴포넌트 수 감소**: 68개 → **9개** (관리 포인트 87% 감소)
2. **Config Panel 통합**: 28개 → **1개** (DynamicConfigPanel)
3. **유연한 UI 변경**: 컴포넌트 교체 없이 속성 변경만으로 UI 모드 전환 가능
4. **Low-Code 확장성**: 새로운 유형의 입력 방식이 필요할 때 코딩 없이 DB 설정만으로 추가 가능
---
## 7. 리스크 및 대응 방안
| 리스크 | 영향도 | 대응 방안 |
| :----------------------- | :----: | :-------------------------------- |
| 기존 화면 호환성 깨짐 | 높음 | 병행 운영 + 하위 호환성 유지 |
| 마이그레이션 데이터 손실 | 높음 | 백업 필수 + 롤백 스크립트 준비 |
| 개발자 학습 곡선 | 중간 | 상세 가이드 문서 + 예제 코드 제공 |
| 성능 저하 (동적 렌더링) | 중간 | 메모이제이션 + 지연 로딩 적용 |
---
## 8. 현재 컴포넌트 매핑 분석
### 8.1 Registry 등록 컴포넌트 전수 조사 (44개)
현재 `frontend/lib/registry/components/`에 등록된 모든 컴포넌트의 통합 가능 여부를 분석했습니다.
#### UnifiedInput으로 통합 (4개)
| 현재 컴포넌트 | 매핑 속성 | 비고 |
| :------------- | :--------------- | :------------- |
| text-input | `type: "text"` | |
| number-input | `type: "number"` | |
| slider-basic | `type: "slider"` | 속성 추가 필요 |
| button-primary | `type: "button"` | 별도 검토 |
#### UnifiedSelect로 통합 (8개)
| 현재 컴포넌트 | 매핑 속성 | 비고 |
| :------------------------ | :----------------------------------- | :------------- |
| select-basic | `mode: "dropdown"` | |
| checkbox-basic | `mode: "check"` | |
| radio-basic | `mode: "radio"` | |
| toggle-switch | `mode: "toggle"` | 속성 추가 필요 |
| autocomplete-search-input | `mode: "dropdown", searchable: true` | |
| entity-search-input | `source: "entity"` | |
| mail-recipient-selector | `mode: "multi", type: "email"` | 복합 컴포넌트 |
| location-swap-selector | `mode: "swap"` | 특수 UI |
#### UnifiedDate로 통합 (1개)
| 현재 컴포넌트 | 매핑 속성 | 비고 |
| :------------ | :------------- | :--- |
| date-input | `type: "date"` | |
#### UnifiedText로 통합 (1개)
| 현재 컴포넌트 | 매핑 속성 | 비고 |
| :------------- | :--------------- | :--- |
| textarea-basic | `mode: "simple"` | |
#### UnifiedMedia로 통합 (3개)
| 현재 컴포넌트 | 매핑 속성 | 비고 |
| :------------ | :------------------------------ | :--- |
| file-upload | `type: "file"` | |
| image-widget | `type: "image"` | |
| image-display | `type: "image", readonly: true` | |
#### UnifiedList로 통합 (8개)
| 현재 컴포넌트 | 매핑 속성 | 비고 |
| :-------------------- | :------------------------------------ | :------------ |
| table-list | `viewMode: "table"` | |
| card-display | `viewMode: "card"` | |
| repeater-field-group | `editable: true` | |
| modal-repeater-table | `viewMode: "table", modal: true` | |
| simple-repeater-table | `viewMode: "table", simple: true` | |
| repeat-screen-modal | `viewMode: "card", modal: true` | |
| table-search-widget | `viewMode: "table", searchable: true` | |
| tax-invoice-list | `viewMode: "table", bizType: "tax"` | 특수 비즈니스 |
#### UnifiedLayout으로 통합 (4개)
| 현재 컴포넌트 | 매핑 속성 | 비고 |
| :------------------ | :-------------------------- | :------------- |
| split-panel-layout | `type: "split"` | |
| split-panel-layout2 | `type: "split", version: 2` | |
| divider-line | `type: "divider"` | 속성 추가 필요 |
| screen-split-panel | `type: "screen-embed"` | 화면 임베딩 |
#### UnifiedGroup으로 통합 (5개)
| 현재 컴포넌트 | 매핑 속성 | 비고 |
| :------------------- | :--------------------- | :------------ |
| accordion-basic | `type: "accordion"` | |
| tabs | `type: "tabs"` | |
| section-paper | `type: "section"` | |
| section-card | `type: "card-section"` | |
| universal-form-modal | `type: "form-modal"` | 복합 컴포넌트 |
#### UnifiedBiz로 통합 (7개)
| 현재 컴포넌트 | 매핑 속성 | 비고 |
| :-------------------- | :------------------------ | :--------------- |
| flow-widget | `type: "flow"` | 플로우 관리 |
| rack-structure | `type: "rack"` | 창고 렉 구조 |
| map | `type: "map"` | 지도 |
| numbering-rule | `type: "numbering"` | 채번 규칙 |
| category-manager | `type: "category"` | 카테고리 관리 |
| customer-item-mapping | `type: "mapping"` | 거래처-품목 매핑 |
| related-data-buttons | `type: "related-buttons"` | 연관 데이터 |
#### 별도 검토 필요 (3개)
| 현재 컴포넌트 | 문제점 | 제안 |
| :-------------------------- | :------------------- | :------------------------------ |
| conditional-container | 조건부 렌더링 로직 | 공통 속성으로 분리 |
| selected-items-detail-input | 복합 (선택+상세입력) | UnifiedList + UnifiedGroup 조합 |
| text-display | 읽기 전용 텍스트 | UnifiedInput (readonly: true) |
### 8.2 매핑 분석 결과
```
┌─────────────────────────────────────────────────────────┐
│ 전체 44개 컴포넌트 분석 결과 │
├─────────────────────────────────────────────────────────┤
│ ✅ 즉시 통합 가능 : 36개 (82%) │
│ ⚠️ 속성 추가 후 통합 : 5개 (11%) │
│ 🔄 별도 검토 필요 : 3개 (7%) │
└─────────────────────────────────────────────────────────┘
```
### 8.3 속성 확장 필요 사항
#### UnifiedInput 속성 확장
```typescript
// 기존
type: "text" | "number" | "password";
// 확장
type: "text" | "number" | "password" | "slider" | "color" | "button";
```
#### UnifiedSelect 속성 확장
```typescript
// 기존
mode: "dropdown" | "radio" | "check" | "tag";
// 확장
mode: "dropdown" | "radio" | "check" | "tag" | "toggle" | "swap";
```
#### UnifiedLayout 속성 확장
```typescript
// 기존
type: "grid" | "split" | "flex";
// 확장
type: "grid" | "split" | "flex" | "divider" | "screen-embed";
```
### 8.4 조건부 렌더링 공통화
`conditional-container`의 기능을 모든 컴포넌트에서 사용 가능한 공통 속성으로 분리합니다.
```typescript
// 모든 Unified 컴포넌트에 적용 가능한 공통 속성
interface BaseUnifiedProps {
// ... 기존 속성
/** 조건부 렌더링 설정 */
conditional?: {
enabled: boolean;
field: string; // 참조할 필드명
operator: "=" | "!=" | ">" | "<" | "in" | "notIn";
value: any; // 비교 값
hideOnFalse?: boolean; // false일 때 숨김 (기본: true)
};
}
```
---
## 9. 계층 구조(Hierarchy) 컴포넌트 전략
### 9.1 현재 계층 구조 지원 현황
DB 테이블 `cascading_hierarchy_group`에서 4가지 계층 타입을 지원합니다:
| 타입 | 설명 | 예시 |
| :----------------- | :---------------------- | :--------------- |
| **MULTI_TABLE** | 다중 테이블 계층 | 국가 > 도시 > 구 |
| **SELF_REFERENCE** | 자기 참조 (단일 테이블) | 조직도, 메뉴 |
| **BOM** | 자재명세서 구조 | 부품 > 하위부품 |
| **TREE** | 일반 트리 | 카테고리 |
### 9.2 통합 방안: UnifiedHierarchy 신설 (10번째 컴포넌트)
계층 구조는 일반 입력/표시 위젯과 성격이 다르므로 **별도 컴포넌트로 분리**합니다.
```typescript
interface UnifiedHierarchyProps {
/** 계층 유형 */
type: "tree" | "org" | "bom" | "cascading";
/** 표시 방식 */
viewMode: "tree" | "table" | "indent" | "dropdown";
/** 계층 그룹 코드 (cascading_hierarchy_group 연동) */
source: string;
/** 편집 가능 여부 */
editable?: boolean;
/** 드래그 정렬 가능 */
draggable?: boolean;
/** BOM 수량 표시 (BOM 타입 전용) */
showQty?: boolean;
/** 최대 레벨 제한 */
maxLevel?: number;
}
```
### 9.3 활용 예시
| 설정 | 결과 |
| :---------------------------------------- | :------------------------- |
| `type: "tree", viewMode: "tree"` | 카테고리 트리뷰 |
| `type: "org", viewMode: "tree"` | 조직도 |
| `type: "bom", viewMode: "indent"` | BOM 들여쓰기 테이블 |
| `type: "cascading", viewMode: "dropdown"` | 연쇄 셀렉트 (국가>도시>구) |
---
## 10. 최종 통합 컴포넌트 목록 (10개)
| # | 컴포넌트 | 역할 | 커버 범위 |
| :-: | :------------------- | :------------- | :----------------------------------- |
| 1 | **UnifiedInput** | 단일 값 입력 | text, number, slider, button 등 |
| 2 | **UnifiedSelect** | 선택 입력 | dropdown, radio, checkbox, toggle 등 |
| 3 | **UnifiedDate** | 날짜/시간 입력 | date, datetime, time, range |
| 4 | **UnifiedText** | 다중 행 텍스트 | textarea, rich editor, markdown |
| 5 | **UnifiedMedia** | 파일/미디어 | file, image, video, audio |
| 6 | **UnifiedList** | 데이터 목록 | table, card, repeater, kanban |
| 7 | **UnifiedLayout** | 레이아웃 배치 | grid, split, flex, divider |
| 8 | **UnifiedGroup** | 콘텐츠 그룹화 | tabs, accordion, section, modal |
| 9 | **UnifiedBiz** | 비즈니스 특화 | flow, rack, map, numbering 등 |
| 10 | **UnifiedHierarchy** | 계층 구조 | tree, org, bom, cascading |
---
## 11. 연쇄관계 관리 메뉴 통합 전략
### 11.1 현재 연쇄관계 관리 현황
**관리 메뉴**: `연쇄 드롭다운 통합 관리` (6개 탭)
| 탭 | DB 테이블 | 실제 데이터 | 복잡도 |
| :--------------- | :--------------------------------------- | :---------: | :----: |
| 2단계 연쇄관계 | `cascading_relation` | 2건 | 낮음 |
| 다단계 계층 | `cascading_hierarchy_group/level` | 1건 | 높음 |
| 조건부 필터 | `cascading_condition` | 0건 | 중간 |
| 자동 입력 | `cascading_auto_fill_group/mapping` | 0건 | 낮음 |
| 상호 배제 | `cascading_mutual_exclusion` | 0건 | 낮음 |
| 카테고리 값 연쇄 | `category_value_cascading_group/mapping` | 2건 | 중간 |
### 11.2 통합 방향: 속성 기반 vs 공통 정의
#### 판단 기준
| 기능 | 재사용 빈도 | 설정 복잡도 | 권장 방식 |
| :--------------- | :---------: | :---------: | :----------------------- |
| 2단계 연쇄 | 낮음 | 간단 | **속성에 inline 정의** |
| 다단계 계층 | 높음 | 복잡 | **공통 정의 유지** |
| 조건부 필터 | 낮음 | 간단 | **속성에 inline 정의** |
| 자동 입력 | 낮음 | 간단 | **속성에 inline 정의** |
| 상호 배제 | 낮음 | 간단 | **속성에 inline 정의** |
| 카테고리 값 연쇄 | 중간 | 중간 | **카테고리 관리와 통합** |
### 11.3 속성 통합 설계
#### 2단계 연쇄 → UnifiedSelect 속성
```typescript
// AS-IS: 별도 관리 메뉴에서 정의 후 참조
<SelectWidget cascadingRelation="WAREHOUSE_LOCATION" />
// TO-BE: 컴포넌트 속성에서 직접 정의
<UnifiedSelect
source="db"
table="warehouse_location"
valueColumn="location_code"
labelColumn="location_name"
cascading={{
parentField: "warehouse_code", // 같은 화면 내 부모 필드
filterColumn: "warehouse_code", // 필터링할 컬럼
clearOnChange: true // 부모 변경 시 초기화
}}
/>
```
#### 조건부 필터 → 공통 conditional 속성
```typescript
// AS-IS: 별도 관리 메뉴에서 조건 정의
// cascading_condition 테이블에 저장
// TO-BE: 모든 컴포넌트에 공통 속성으로 적용
<UnifiedInput
conditional={{
enabled: true,
field: "order_type", // 참조할 필드
operator: "=", // 비교 연산자
value: "EXPORT", // 비교 값
action: "show", // show | hide | disable | enable
}}
/>
```
#### 자동 입력 → autoFill 속성
```typescript
// AS-IS: cascading_auto_fill_group 테이블에 정의
// TO-BE: 컴포넌트 속성에서 직접 정의
<UnifiedInput
autoFill={{
enabled: true,
sourceTable: "company_mng", // 조회할 테이블
filterColumn: "company_code", // 필터링 컬럼
userField: "companyCode", // 사용자 정보 필드
displayColumn: "company_name", // 표시할 컬럼
}}
/>
```
#### 상호 배제 → mutualExclusion 속성
```typescript
// AS-IS: cascading_mutual_exclusion 테이블에 정의
// TO-BE: 컴포넌트 속성에서 직접 정의
<UnifiedSelect
mutualExclusion={{
enabled: true,
targetField: "sub_category", // 상호 배제 대상 필드
type: "exclusive", // exclusive | inclusive
}}
/>
```
### 11.4 관리 메뉴 정리 계획
| 현재 메뉴 | TO-BE | 비고 |
| :-------------------------- | :----------------------- | :-------------------- |
| **연쇄 드롭다운 통합 관리** | **삭제** | 6개 탭 전체 제거 |
| ├─ 2단계 연쇄관계 | UnifiedSelect 속성 | inline 정의 |
| ├─ 다단계 계층 | **테이블관리로 이동** | 복잡한 구조 유지 필요 |
| ├─ 조건부 필터 | 공통 conditional 속성 | 모든 컴포넌트에 적용 |
| ├─ 자동 입력 | autoFill 속성 | 컴포넌트별 정의 |
| ├─ 상호 배제 | mutualExclusion 속성 | 컴포넌트별 정의 |
| └─ 카테고리 값 연쇄 | **카테고리 관리로 이동** | 기존 메뉴 통합 |
### 11.5 DB 테이블 정리 (Phase 5)
| 테이블 | 조치 | 시점 |
| :--------------------------- | :----------------------- | :------ |
| `cascading_relation` | 마이그레이션 후 삭제 | Phase 5 |
| `cascading_condition` | 삭제 (데이터 없음) | Phase 5 |
| `cascading_auto_fill_*` | 삭제 (데이터 없음) | Phase 5 |
| `cascading_mutual_exclusion` | 삭제 (데이터 없음) | Phase 5 |
| `cascading_hierarchy_*` | **유지** | - |
| `category_value_cascading_*` | **유지** (카테고리 관리) | - |
### 11.6 마이그레이션 스크립트 필요 항목
```sql
-- cascading_relation → 화면 레이아웃 데이터로 마이그레이션
-- 기존 2건의 연쇄관계를 사용하는 화면을 찾아서
-- 해당 컴포넌트의 cascading 속성으로 변환
-- 예시: WAREHOUSE_LOCATION 연쇄관계
-- 이 관계를 사용하는 화면의 컴포넌트에
-- cascading: { parentField: "warehouse_code", filterColumn: "warehouse_code" }
-- 속성 추가
```
---
## 12. 최종 아키텍처 요약
### 12.1 통합 컴포넌트 (10개)
| # | 컴포넌트 | 역할 |
| :-: | :------------------- | :--------------------------------------- |
| 1 | **UnifiedInput** | 단일 값 입력 (text, number, slider 등) |
| 2 | **UnifiedSelect** | 선택 입력 (dropdown, radio, checkbox 등) |
| 3 | **UnifiedDate** | 날짜/시간 입력 |
| 4 | **UnifiedText** | 다중 행 텍스트 (textarea, rich editor) |
| 5 | **UnifiedMedia** | 파일/미디어 (file, image) |
| 6 | **UnifiedList** | 데이터 목록 (table, card, repeater) |
| 7 | **UnifiedLayout** | 레이아웃 배치 (grid, split, flex) |
| 8 | **UnifiedGroup** | 콘텐츠 그룹화 (tabs, accordion, section) |
| 9 | **UnifiedBiz** | 비즈니스 특화 (flow, rack, map 등) |
| 10 | **UnifiedHierarchy** | 계층 구조 (tree, org, bom, cascading) |
### 12.2 공통 속성 (모든 컴포넌트에 적용)
```typescript
interface BaseUnifiedProps {
// 기본 속성
id: string;
label?: string;
required?: boolean;
readonly?: boolean;
disabled?: boolean;
// 스타일
style?: ComponentStyle;
className?: string;
// 조건부 렌더링 (conditional-container 대체)
conditional?: {
enabled: boolean;
field: string;
operator:
| "="
| "!="
| ">"
| "<"
| "in"
| "notIn"
| "isEmpty"
| "isNotEmpty";
value: any;
action: "show" | "hide" | "disable" | "enable";
};
// 자동 입력 (autoFill 대체)
autoFill?: {
enabled: boolean;
sourceTable: string;
filterColumn: string;
userField: "companyCode" | "userId" | "deptCode";
displayColumn: string;
};
// 유효성 검사
validation?: ValidationRule[];
}
```
### 12.3 UnifiedSelect 전용 속성
```typescript
interface UnifiedSelectProps extends BaseUnifiedProps {
// 표시 모드
mode: "dropdown" | "radio" | "check" | "tag" | "toggle" | "swap";
// 데이터 소스
source: "static" | "code" | "db" | "api" | "entity";
// static 소스
options?: Array<{ value: string; label: string }>;
// db 소스
table?: string;
valueColumn?: string;
labelColumn?: string;
// code 소스
codeGroup?: string;
// 연쇄 관계 (cascading_relation 대체)
cascading?: {
parentField: string; // 부모 필드명
filterColumn: string; // 필터링할 컬럼
clearOnChange?: boolean; // 부모 변경 시 초기화
};
// 상호 배제 (mutual_exclusion 대체)
mutualExclusion?: {
enabled: boolean;
targetField: string; // 상호 배제 대상
type: "exclusive" | "inclusive";
};
// 다중 선택
multiple?: boolean;
maxSelect?: number;
}
```
### 12.4 관리 메뉴 정리 결과
| AS-IS | TO-BE |
| :---------------------------- | :----------------------------------- |
| 연쇄 드롭다운 통합 관리 (6탭) | **삭제** |
| - 2단계 연쇄관계 | → UnifiedSelect.cascading 속성 |
| - 다단계 계층 | → 테이블관리 > 계층 구조 설정 |
| - 조건부 필터 | → 공통 conditional 속성 |
| - 자동 입력 | → 공통 autoFill 속성 |
| - 상호 배제 | → UnifiedSelect.mutualExclusion 속성 |
| - 카테고리 값 연쇄 | → 카테고리 관리와 통합 |
---
## 13. 주의사항
> **기존 컴포넌트 삭제 금지**
> 모든 Phase에서 기존 컴포넌트는 삭제하지 않고 **병행 운영**합니다.
> 레거시 정리는 Phase 5에서 충분한 안정화 후 별도 검토합니다.
> **연쇄관계 마이그레이션 필수**
> 관리 메뉴 삭제 전 기존 `cascading_relation` 데이터(2건)를
> 해당 화면의 컴포넌트 속성으로 마이그레이션해야 합니다.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
# 프로젝트 진행 상황 (2025-11-20)
## 작업 개요: 디지털 트윈 3D 야드 고도화 (동적 계층 구조)
### 1. 핵심 변경 사항
기존의 고정된 `Area` -> `Location` 2단계 구조를 유연한 **N-Level 동적 계층 구조**로 변경하고, 공간적 제약을 강화했습니다.
### 2. 완료된 작업
#### 데이터베이스
- **마이그레이션 실행**: `db/migrations/042_refactor_digital_twin_hierarchy.sql`
- **스키마 변경**:
- `digital_twin_layout` 테이블에 `hierarchy_config` (JSONB) 컬럼 추가
- `digital_twin_objects` 테이블에 `hierarchy_level`, `parent_key`, `external_key` 컬럼 추가
- 기존 하드코딩된 테이블 매핑 컬럼 제거
#### 백엔드 (Node.js)
- **API 추가/수정**:
- `POST /api/digital-twin/data/hierarchy`: 계층 설정에 따른 전체 데이터 조회
- `POST /api/digital-twin/data/children`: 특정 부모의 하위 데이터 조회
- 기존 레거시 API (`getWarehouses` 등) 호환성 유지
- **컨트롤러 수정**:
- `digitalTwinDataController.ts`: 동적 쿼리 생성 로직 구현
- `digitalTwinLayoutController.ts`: 레이아웃 저장/수정 시 `hierarchy_config` 및 객체 계층 정보 처리
#### 프론트엔드 (React)
- **신규 컴포넌트**: `HierarchyConfigPanel.tsx`
- 레벨 추가/삭제, 테이블 및 컬럼 매핑 설정 UI
- **유틸리티**: `spatialContainment.ts`
- `validateSpatialContainment`: 자식 객체가 부모 객체 내부에 있는지 검증 (AABB)
- `updateChildrenPositions`: 부모 이동 시 자식 객체 자동 이동 (그룹 이동)
- **에디터 통합 (`DigitalTwinEditor.tsx`)**:
- `HierarchyConfigPanel` 적용
- 동적 데이터 로드 로직 구현
- 3D 캔버스 드래그앤드롭 시 공간적 종속성 검증 적용
- 객체 이동 시 그룹 이동 적용
### 3. 현재 상태
- **백엔드 서버**: 재시작 완료, 정상 동작 중 (PostgreSQL 연결 이슈 해결됨)
- **DB**: 마이그레이션 스크립트 실행 완료
### 4. 다음 단계 (테스트 필요)
새로운 세션에서 다음 시나리오를 테스트해야 합니다:
1. **계층 설정**: 에디터에서 창고 -> 구역(Lv1) -> 위치(Lv2) 설정 및 매핑 저장
2. **배치 검증**:
- 구역 배치 후, 위치를 구역 **내부**에 배치 (성공해야 함)
- 위치를 구역 **외부**에 배치 (실패해야 함)
3. **이동 검증**: 구역 이동 시 내부의 위치들도 같이 따라오는지 확인
### 5. 관련 파일
- `frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinEditor.tsx`
- `frontend/components/admin/dashboard/widgets/yard-3d/HierarchyConfigPanel.tsx`
- `frontend/components/admin/dashboard/widgets/yard-3d/spatialContainment.ts`
- `backend-node/src/controllers/digitalTwinDataController.ts`
- `backend-node/src/routes/digitalTwinRoutes.ts`
- `db/migrations/042_refactor_digital_twin_hierarchy.sql`

View File

@ -1,311 +0,0 @@
# 🎨 제어관리 - 데이터 연결 설정 UI 재설계 계획서
## 📋 프로젝트 개요
### 목표
- 기존 모달 기반 필드 매핑을 메인 화면으로 통합
- 중복된 테이블 선택 과정 제거
- 시각적 필드 연결 매핑 구현
- 좌우 분할 레이아웃으로 정보 가시성 향상
### 현재 문제점
- ❌ **이중 작업**: 테이블을 3번 선택해야 함 (더블클릭 → 모달 → 재선택)
- ❌ **혼란스러운 UX**: 사전 선택의 의미가 없어짐
- ❌ **불필요한 모달**: 연결 설정이 메인 기능인데 숨겨져 있음
- ❌ **시각적 피드백 부족**: 필드 매핑 관계가 명확하지 않음
## 🎯 새로운 UI 구조
### 레이아웃 구성
```
┌─────────────────────────────────────────────────────────────┐
│ 제어관리 - 데이터 연결 설정 │
├─────────────────────────────────────────────────────────────┤
│ 좌측 패널 (30%) │ 우측 패널 (70%) │
│ - 연결 타입 선택 │ - 단계별 설정 UI │
│ - 매핑 정보 모니터링 │ - 시각적 필드 매핑 │
│ - 상세 설정 목록 │ - 실시간 연결선 표시 │
│ - 액션 버튼 │ - 드래그 앤 드롭 지원 │
└─────────────────────────────────────────────────────────────┘
```
## 🔧 구현 단계
### Phase 1: 기본 구조 구축
- [ ] 좌우 분할 레이아웃 컴포넌트 생성
- [ ] 기존 모달 컴포넌트들을 메인 화면용으로 리팩토링
- [ ] 연결 타입 선택 컴포넌트 구현
### Phase 2: 좌측 패널 구현
- [ ] 연결 타입 선택 (데이터 저장 / 외부 호출)
- [ ] 실시간 매핑 정보 표시
- [ ] 매핑 상세 목록 컴포넌트
- [ ] 고급 설정 패널
### Phase 3: 우측 패널 구현
- [ ] 단계별 진행 UI (연결 → 테이블 → 매핑)
- [ ] 시각적 필드 매핑 영역
- [ ] SVG 기반 연결선 시스템
- [ ] 드래그 앤 드롭 매핑 기능
### Phase 4: 고급 기능
- [ ] 실시간 검증 및 피드백
- [ ] 매핑 미리보기 기능
- [ ] 설정 저장/불러오기
- [ ] 테스트 실행 기능
## 📁 파일 구조
### 새로 생성할 컴포넌트
```
frontend/components/dataflow/connection/redesigned/
├── DataConnectionDesigner.tsx # 메인 컨테이너
├── LeftPanel/
│ ├── ConnectionTypeSelector.tsx # 연결 타입 선택
│ ├── MappingInfoPanel.tsx # 매핑 정보 표시
│ ├── MappingDetailList.tsx # 매핑 상세 목록
│ ├── AdvancedSettings.tsx # 고급 설정
│ └── ActionButtons.tsx # 액션 버튼들
├── RightPanel/
│ ├── StepProgress.tsx # 단계 진행 표시
│ ├── ConnectionStep.tsx # 1단계: 연결 선택
│ ├── TableStep.tsx # 2단계: 테이블 선택
│ ├── FieldMappingStep.tsx # 3단계: 필드 매핑
│ └── VisualMapping/
│ ├── FieldMappingCanvas.tsx # 시각적 매핑 캔버스
│ ├── FieldColumn.tsx # 필드 컬럼 컴포넌트
│ ├── ConnectionLine.tsx # SVG 연결선
│ └── MappingControls.tsx # 매핑 제어 도구
└── types/
└── redesigned.ts # 타입 정의
```
### 수정할 기존 파일
```
frontend/components/dataflow/connection/
├── DataSaveSettings.tsx # 새 UI로 교체
├── ConnectionSelectionPanel.tsx # 재사용을 위한 리팩토링
├── TableSelectionPanel.tsx # 재사용을 위한 리팩토링
└── ActionFieldMappings.tsx # 레거시 처리
```
## 🎨 UI 컴포넌트 상세
### 1. 연결 타입 선택 (ConnectionTypeSelector)
```typescript
interface ConnectionType {
id: "data_save" | "external_call";
label: string;
description: string;
icon: React.ReactNode;
}
const connectionTypes: ConnectionType[] = [
{
id: "data_save",
label: "데이터 저장",
description: "INSERT/UPDATE/DELETE 작업",
icon: <Database />,
},
{
id: "external_call",
label: "외부 호출",
description: "API/Webhook 호출",
icon: <Globe />,
},
];
```
### 2. 시각적 필드 매핑 (FieldMappingCanvas)
```typescript
interface FieldMapping {
id: string;
fromField: ColumnInfo;
toField: ColumnInfo;
transformRule?: string;
isValid: boolean;
validationMessage?: string;
}
interface MappingLine {
id: string;
fromX: number;
fromY: number;
toX: number;
toY: number;
isValid: boolean;
isHovered: boolean;
}
```
### 3. 매핑 정보 패널 (MappingInfoPanel)
```typescript
interface MappingStats {
totalMappings: number;
validMappings: number;
invalidMappings: number;
missingRequiredFields: number;
estimatedRows: number;
actionType: "INSERT" | "UPDATE" | "DELETE";
}
```
## 🔄 데이터 플로우
### 상태 관리
```typescript
interface DataConnectionState {
// 기본 설정
connectionType: "data_save" | "external_call";
currentStep: 1 | 2 | 3;
// 연결 정보
fromConnection?: Connection;
toConnection?: Connection;
fromTable?: TableInfo;
toTable?: TableInfo;
// 매핑 정보
fieldMappings: FieldMapping[];
mappingStats: MappingStats;
// UI 상태
selectedMapping?: string;
isLoading: boolean;
validationErrors: ValidationError[];
}
```
### 이벤트 핸들링
```typescript
interface DataConnectionActions {
// 연결 타입
setConnectionType: (type: "data_save" | "external_call") => void;
// 단계 진행
goToStep: (step: 1 | 2 | 3) => void;
// 연결/테이블 선택
selectConnection: (type: "from" | "to", connection: Connection) => void;
selectTable: (type: "from" | "to", table: TableInfo) => void;
// 필드 매핑
createMapping: (fromField: ColumnInfo, toField: ColumnInfo) => void;
updateMapping: (mappingId: string, updates: Partial<FieldMapping>) => void;
deleteMapping: (mappingId: string) => void;
// 검증 및 저장
validateMappings: () => Promise<ValidationResult>;
saveMappings: () => Promise<void>;
testExecution: () => Promise<TestResult>;
}
```
## 🎯 사용자 경험 (UX) 개선점
### Before (기존)
1. 테이블 더블클릭 → 화면에 표시
2. 모달 열기 → 다시 테이블 선택
3. 외부 커넥션 설정 → 또 다시 테이블 선택
4. 필드 매핑 → 텍스트 기반 매핑
### After (개선)
1. **연결 타입 선택** → 목적 명확화
2. **연결 선택** → 한 번에 FROM/TO 설정
3. **테이블 선택** → 즉시 필드 정보 로드
4. **시각적 매핑** → 드래그 앤 드롭으로 직관적 연결
## 🚀 구현 우선순위
### 🔥 High Priority
1. **기본 레이아웃** - 좌우 분할 구조
2. **연결 타입 선택** - 데이터 저장/외부 호출
3. **단계별 진행** - 연결 → 테이블 → 매핑
4. **기본 필드 매핑** - 드래그 앤 드롭 없이 클릭 기반
### 🔶 Medium Priority
1. **시각적 연결선** - SVG 기반 라인 표시
2. **실시간 검증** - 타입 호환성 체크
3. **매핑 정보 패널** - 통계 및 상태 표시
4. **드래그 앤 드롭** - 고급 매핑 기능
### 🔵 Low Priority
1. **고급 설정** - 트랜잭션, 배치 설정
2. **미리보기 기능** - 데이터 변환 미리보기
3. **설정 템플릿** - 자주 사용하는 매핑 저장
4. **성능 최적화** - 대용량 테이블 처리
## 📅 개발 일정
### Week 1: 기본 구조
- [ ] 레이아웃 컴포넌트 생성
- [ ] 연결 타입 선택 구현
- [ ] 기존 컴포넌트 리팩토링
### Week 2: 핵심 기능
- [ ] 단계별 진행 UI
- [ ] 연결/테이블 선택 통합
- [ ] 기본 필드 매핑 구현
### Week 3: 시각적 개선
- [ ] SVG 연결선 시스템
- [ ] 드래그 앤 드롭 매핑
- [ ] 실시간 검증 기능
### Week 4: 완성 및 테스트
- [ ] 고급 기능 구현
- [ ] 통합 테스트
- [ ] 사용자 테스트 및 피드백 반영
## 🔍 기술적 고려사항
### 성능 최적화
- **가상화**: 대용량 필드 목록 처리
- **메모이제이션**: 불필요한 리렌더링 방지
- **지연 로딩**: 필요한 시점에만 데이터 로드
### 접근성
- **키보드 네비게이션**: 모든 기능을 키보드로 접근 가능
- **스크린 리더**: 시각적 매핑의 대체 텍스트 제공
- **색상 대비**: 연결선과 상태 표시의 명확한 구분
### 확장성
- **플러그인 구조**: 새로운 연결 타입 쉽게 추가
- **커스텀 변환**: 사용자 정의 데이터 변환 규칙
- **API 확장**: 외부 시스템과의 연동 지원
---
## 🎯 다음 단계
이 계획서를 바탕으로 **Phase 1부터 순차적으로 구현**을 시작하겠습니다.
**첫 번째 작업**: 좌우 분할 레이아웃과 연결 타입 선택 컴포넌트 구현
구현을 시작하시겠어요? 🚀

View File

@ -1,150 +0,0 @@
# 작업 이력 관리 시스템 설치 가이드
## 📋 개요
작업 이력 관리 시스템이 추가되었습니다. 입고/출고/이송/정비 작업을 관리하고 통계를 확인할 수 있습니다.
## 🚀 설치 방법
### 1. 데이터베이스 마이그레이션 실행
PostgreSQL 데이터베이스에 작업 이력 테이블을 생성해야 합니다.
```bash
# 방법 1: psql 명령어 사용 (로컬 PostgreSQL)
psql -U postgres -d plm -f db/migrations/20241020_create_work_history.sql
# 방법 2: Docker 컨테이너 사용
docker exec -i <DB_CONTAINER_NAME> psql -U postgres -d plm < db/migrations/20241020_create_work_history.sql
# 방법 3: pgAdmin 또는 DBeaver 사용
# db/migrations/20241020_create_work_history.sql 파일을 열어서 실행
```
### 2. 백엔드 재시작
```bash
cd backend-node
npm run dev
```
### 3. 프론트엔드 확인
대시보드 편집 화면에서 다음 위젯들을 추가할 수 있습니다:
- **작업 이력**: 작업 목록을 테이블 형식으로 표시
- **운송 통계**: 오늘 작업, 총 운송량, 정시 도착률 등 통계 표시
## 📊 주요 기능
### 작업 이력 위젯
- 작업 번호, 일시, 유형, 차량, 경로, 화물, 중량, 상태 표시
- 유형별 필터링 (입고/출고/이송/정비)
- 상태별 필터링 (대기/진행중/완료/취소)
- 실시간 자동 새로고침
### 운송 통계 위젯
- 오늘 작업 건수 및 완료율
- 총 운송량 (톤)
- 누적 거리 (km)
- 정시 도착률 (%)
- 작업 유형별 분포 차트
## 🔧 API 엔드포인트
### 작업 이력 관리
- `GET /api/work-history` - 작업 이력 목록 조회
- `GET /api/work-history/:id` - 작업 이력 단건 조회
- `POST /api/work-history` - 작업 이력 생성
- `PUT /api/work-history/:id` - 작업 이력 수정
- `DELETE /api/work-history/:id` - 작업 이력 삭제
### 통계 및 분석
- `GET /api/work-history/stats` - 작업 이력 통계
- `GET /api/work-history/trend?months=6` - 월별 추이
- `GET /api/work-history/routes?limit=5` - 주요 운송 경로
## 📝 샘플 데이터
마이그레이션 실행 시 자동으로 4건의 샘플 데이터가 생성됩니다:
1. 입고 작업 (완료)
2. 출고 작업 (진행중)
3. 이송 작업 (대기)
4. 정비 작업 (완료)
## 🎯 사용 방법
### 1. 대시보드에 위젯 추가
1. 대시보드 편집 모드로 이동
2. 상단 메뉴에서 "위젯 추가" 선택
3. "작업 이력" 또는 "운송 통계" 선택
4. 원하는 위치에 배치
5. 저장
### 2. 작업 이력 필터링
- 유형 선택: 전체/입고/출고/이송/정비
- 상태 선택: 전체/대기/진행중/완료/취소
- 새로고침 버튼으로 수동 갱신
### 3. 통계 확인
운송 통계 위젯에서 다음 정보를 확인할 수 있습니다:
- 오늘 작업 건수
- 완료율
- 총 운송량
- 정시 도착률
- 작업 유형별 분포
## 🔍 문제 해결
### 데이터가 표시되지 않는 경우
1. 데이터베이스 마이그레이션이 실행되었는지 확인
2. 백엔드 서버가 실행 중인지 확인
3. 브라우저 콘솔에서 API 에러 확인
### API 에러가 발생하는 경우
```bash
# 백엔드 로그 확인
cd backend-node
npm run dev
```
### 위젯이 표시되지 않는 경우
1. 프론트엔드 재시작
2. 브라우저 캐시 삭제
3. 페이지 새로고침
## 📚 관련 파일
### 백엔드
- `backend-node/src/types/workHistory.ts` - 타입 정의
- `backend-node/src/services/workHistoryService.ts` - 비즈니스 로직
- `backend-node/src/controllers/workHistoryController.ts` - API 컨트롤러
- `backend-node/src/routes/workHistoryRoutes.ts` - 라우트 정의
### 프론트엔드
- `frontend/types/workHistory.ts` - 타입 정의
- `frontend/components/dashboard/widgets/WorkHistoryWidget.tsx` - 작업 이력 위젯
- `frontend/components/dashboard/widgets/TransportStatsWidget.tsx` - 운송 통계 위젯
### 데이터베이스
- `db/migrations/20241020_create_work_history.sql` - 테이블 생성 스크립트
## 🎉 완료!
작업 이력 관리 시스템이 성공적으로 설치되었습니다!

View File

@ -1,426 +0,0 @@
# 야드 관리 3D - 데이터 바인딩 시스템 재설계
## 1. 개요
### 현재 방식의 문제점
- 고정된 임시 자재 마스터(`temp_material_master`) 테이블에 의존
- 실제 외부 시스템의 자재 데이터와 연동 불가
- 자재 목록이 제한적이고 유연성 부족
- 사용자가 직접 데이터를 선택하거나 입력할 수 없음
### 새로운 방식의 목표
- 차트/리스트 위젯과 동일한 데이터 소스 선택 방식 적용
- DB 커넥션 또는 REST API를 통해 실제 자재 데이터 연동
- 사용자가 자재명, 수량 등을 직접 매핑 및 입력 가능
- 설정되지 않은 요소는 뷰어에서 명확히 표시
---
## 2. 핵심 변경사항
### 2.1 요소(Element) 개념 도입
- 기존: 자재 목록에서 클릭 → 즉시 배치
- 변경: [+ 요소 추가] 버튼 클릭 → 3D 캔버스에 즉시 빈 요소 배치 → 우측 패널이 데이터 바인딩 설정 화면으로 전환
### 2.2 데이터 소스 선택
- 현재 DB (내부 PostgreSQL)
- 외부 DB 커넥션
- REST API
### 2.3 데이터 매핑
- 자재명 필드 선택 (데이터 소스에서)
- 수량 필드 선택 (데이터 소스에서)
- 단위 직접 입력 (예: EA, BOX, KG 등)
- 색상 선택
---
## 3. 데이터베이스 스키마 변경
### 3.1 기존 테이블 수정: `yard_material_placement`
```sql
-- 기존 컬럼 변경
ALTER TABLE yard_material_placement
-- 기존 컬럼 제거 (외부 자재 ID 관련)
DROP COLUMN IF EXISTS external_material_id,
-- 데이터 소스 정보 추가
ADD COLUMN data_source_type VARCHAR(20), -- 'database', 'external_db', 'rest_api'
ADD COLUMN data_source_config JSONB, -- 데이터 소스 설정
-- 데이터 바인딩 정보 추가
ADD COLUMN data_binding JSONB, -- 필드 매핑 정보
-- 자재 정보를 NULL 허용으로 변경 (설정 전에는 NULL)
ALTER COLUMN material_code DROP NOT NULL,
ALTER COLUMN material_name DROP NOT NULL,
ALTER COLUMN quantity DROP NOT NULL;
```
### 3.2 data_source_config 구조
```typescript
interface DataSourceConfig {
type: "database" | "external_db" | "rest_api";
// type === 'database' (현재 DB)
query?: string;
// type === 'external_db' (외부 DB)
connectionId?: number;
query?: string;
// type === 'rest_api'
url?: string;
method?: "GET" | "POST";
headers?: Record<string, string>;
queryParams?: Record<string, string>;
body?: string;
dataPath?: string; // 응답에서 데이터 배열 경로 (예: "data.items")
}
```
### 3.3 data_binding 구조
```typescript
interface DataBinding {
// 데이터 소스의 특정 행 선택
selectedRowIndex?: number;
// 필드 매핑 (데이터 소스에서 선택)
materialNameField?: string; // 자재명이 들어있는 컬럼명
quantityField?: string; // 수량이 들어있는 컬럼명
// 단위는 사용자가 직접 입력
unit: string; // 예: "EA", "BOX", "KG", "M" 등
}
```
---
## 4. UI/UX 설계
### 4.1 편집 모드 (YardEditor)
```
┌─────────────────────────────────────────────────────────────┐
│ [← 목록으로] 야드명: A구역 [저장] │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────┐ ┌──────────────────────────┐│
│ │ │ │ ││
│ │ │ │ [+ 요소 추가] ││
│ │ │ │ ││
│ │ 3D 캔버스 │ │ ┌────────────────────┐ ││
│ │ │ │ │ □ 요소 1 │ ││
│ │ │ │ │ 자재: 철판 A │ ││
│ │ │ │ │ 수량: 50 EA │ ││
│ │ │ │ │ [편집] [삭제] │ ││
│ │ │ │ └────────────────────┘ ││
│ │ │ │ ││
│ │ │ │ ┌────────────────────┐ ││
│ │ │ │ │ □ 요소 2 (미설정) │ ││
│ │ │ │ │ 데이터 바인딩 │ ││
│ │ │ │ │ 설정 필요 │ ││
│ │ │ │ │ [설정] [삭제] │ ││
│ │ │ │ └────────────────────┘ ││
│ │ │ │ ││
│ └───────────────────────────┘ └──────────────────────────┘│
│ │
└─────────────────────────────────────────────────────────────┘
```
#### 4.1.1 요소 목록 (우측 패널)
- **[+ 요소 추가]** 버튼: 새 요소 생성
- **요소 카드**:
- 설정 완료: 자재명, 수량 표시 + [편집] [삭제] 버튼
- 미설정: "데이터 바인딩 설정 필요" + [설정] [삭제] 버튼
#### 4.1.2 요소 추가 흐름
```
1. [+ 요소 추가] 클릭
2. 3D 캔버스의 기본 위치(0,0,0)에 회색 반투명 박스로 빈 요소 즉시 배치
3. 요소가 자동 선택됨
4. 우측 패널이 "데이터 바인딩 설정" 화면으로 자동 전환
(요소 목록에서 [설정] 버튼을 클릭해도 동일한 화면)
```
### 4.2 데이터 바인딩 설정 패널 (우측)
**[+ 요소 추가] 버튼 클릭 시 또는 [설정] 버튼 클릭 시 우측 패널이 아래와 같이 변경됩니다:**
```
┌──────────────────────────────────────┐
│ 데이터 바인딩 설정 [← 목록]│
├──────────────────────────────────────┤
│ │
│ ┌─ 1단계: 데이터 소스 선택 ─────────────────────────┐ │
│ │ │ │
│ │ ○ 현재 DB ○ 외부 DB ○ REST API │ │
│ │ │ │
│ │ [현재 DB 선택 시] │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ SELECT material_name, quantity, unit │ │ │
│ │ │ FROM inventory │ │ │
│ │ │ WHERE status = 'available' │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ [실행] 버튼 │ │
│ │ │ │
│ │ [외부 DB 선택 시] │ │
│ │ - 외부 커넥션 선택 드롭다운 │ │
│ │ - SQL 쿼리 입력 │ │
│ │ - [실행] 버튼 │ │
│ │ │ │
│ │ [REST API 선택 시] │ │
│ │ - URL 입력 │ │
│ │ - Method 선택 (GET/POST) │ │
│ │ - Headers, Query Params 설정 │ │
│ │ - [실행] 버튼 │ │
│ │ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌─ 2단계: 쿼리 결과 및 필드 매핑 ──────────────────────┐ │
│ │ │ │
│ │ 쿼리 결과 (5행): │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ material_name │ quantity │ status │ │ │
│ │ │ 철판 A │ 50 │ available │ ○ │ │
│ │ │ 강관 파이프 │ 100 │ available │ ○ │ │
│ │ │ 볼트 세트 │ 500 │ in_stock │ ○ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 필드 매핑: │ │
│ │ 자재명: [material_name ▼] │ │
│ │ 수량: [quantity ▼] │ │
│ │ │ │
│ │ 단위 입력: │ │
│ │ 단위: [EA_____________] │ │
│ │ (예: EA, BOX, KG, M, L 등) │ │
│ │ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌─ 3단계: 배치 설정 ──────────────────────────────────┐ │
│ │ │ │
│ │ 색상: [🎨 #3b82f6] │ │
│ │ │ │
│ │ 크기: │ │
│ │ 너비: [5] 높이: [5] 깊이: [5] │ │
│ │ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ [← 목록으로] [저장] │
└──────────────────────────────────────┘
```
**참고:**
- [← 목록으로] 버튼: 요소 목록 화면으로 돌아갑니다
- [저장] 버튼: 데이터 바인딩 설정을 저장하고 요소 목록 화면으로 돌아갑니다
- 저장하지 않고 나가면 요소는 "미설정" 상태로 남습니다
### 4.3 뷰어 모드 (Yard3DViewer)
#### 4.3.1 설정된 요소
- 정상적으로 3D 박스 렌더링
- 클릭 시 자재명, 수량 정보 표시
#### 4.3.2 미설정 요소
```
┌─────────────────────┐
│ │
│ ⚠️ │
│ │
│ 설정되지 않은 │
│ 요소입니다 │
│ │
└─────────────────────┘
```
- 반투명 회색 박스로 표시
- 클릭 시 "데이터 바인딩이 설정되지 않았습니다" 메시지
---
## 5. 구현 단계
### Phase 1: 데이터베이스 스키마 변경
- [ ] `yard_material_placement` 테이블 수정
- [ ] 마이그레이션 스크립트 작성
- [ ] 기존 데이터 호환성 처리
### Phase 2: 백엔드 API 수정
- [ ] `YardLayoutService.ts` 수정
- `addMaterialPlacement`: 데이터 소스/바인딩 정보 저장
- `updatePlacement`: 데이터 바인딩 업데이트
- `getPlacementsByLayoutId`: 새 필드 포함하여 조회
- [ ] 데이터 소스 실행 로직 추가
- DB 쿼리 실행
- 외부 DB 쿼리 실행
- REST API 호출
### Phase 3: 프론트엔드 타입 정의
- [ ] `types.ts`에 새로운 인터페이스 추가
- `YardElementDataSource`
- `YardElementDataBinding`
- `YardPlacement` 업데이트
### Phase 4: 요소 추가 및 관리
- [ ] `YardEditor.tsx` 수정
- [+ 요소 추가] 버튼 구현
- 빈 요소 생성 로직 (즉시 3D 캔버스에 배치)
- 요소 추가 시 자동으로 해당 요소 선택
- 우측 패널 상태 관리 (요소 목록 ↔ 데이터 바인딩 설정)
- 요소 목록 UI
- 설정/미설정 상태 구분 표시
### Phase 5: 데이터 바인딩 패널
- [ ] `YardElementConfigPanel.tsx` 생성 (우측 패널 컴포넌트)
- [← 목록으로] 버튼으로 요소 목록으로 복귀
- 1단계: 데이터 소스 선택 (DatabaseConfig, ExternalDbConfig, RestApiConfig 재사용)
- 2단계: 쿼리 결과 테이블 + 행 선택 + 필드 매핑
- 자재명 필드 선택 (드롭다운)
- 수량 필드 선택 (드롭다운)
- 단위 직접 입력 (Input)
- 3단계: 배치 설정 (색상, 크기)
- [저장] 버튼으로 설정 저장 및 목록으로 복귀
### Phase 6: 3D 캔버스 렌더링 수정
- [ ] `Yard3DCanvas.tsx` 수정
- 설정된 요소: 기존 렌더링
- 미설정 요소: 회색 반투명 박스 + 경고 아이콘
### Phase 7: 뷰어 모드 수정
- [ ] `Yard3DViewer.tsx` 수정
- 미설정 요소 감지
- 미설정 요소 클릭 시 안내 메시지
### Phase 8: 임시 테이블 제거
- [ ] `temp_material_master` 테이블 삭제
- [ ] 관련 API 및 UI 코드 정리
---
## 6. 데이터 구조 예시
### 6.1 데이터 소스 + 필드 매핑 사용
```json
{
"id": 1,
"yard_layout_id": 1,
"material_code": null,
"material_name": "철판 A타입",
"quantity": 50,
"unit": "EA",
"data_source_type": "database",
"data_source_config": {
"type": "database",
"query": "SELECT material_name, quantity FROM inventory WHERE material_id = 'MAT-001'"
},
"data_binding": {
"selectedRowIndex": 0,
"materialNameField": "material_name",
"quantityField": "quantity",
"unit": "EA"
},
"position_x": 10,
"position_y": 0,
"position_z": 10,
"size_x": 5,
"size_y": 5,
"size_z": 5,
"color": "#ef4444"
}
```
### 6.2 미설정 요소
```json
{
"id": 3,
"yard_layout_id": 1,
"material_code": null,
"material_name": null,
"quantity": null,
"unit": null,
"data_source_type": null,
"data_source_config": null,
"data_binding": null,
"position_x": 30,
"position_y": 0,
"position_z": 30,
"size_x": 5,
"size_y": 5,
"size_z": 5,
"color": "#9ca3af"
}
```
---
## 7. 장점
1. **유연성**: 다양한 데이터 소스 지원 (내부 DB, 외부 DB, REST API)
2. **실시간성**: 실제 시스템의 자재 데이터와 연동 가능
3. **일관성**: 차트/리스트 위젯과 동일한 데이터 소스 선택 방식
4. **사용자 경험**: 데이터 매핑 방식 선택 가능 (자동/수동)
5. **확장성**: 새로운 데이터 소스 타입 추가 용이
6. **명확성**: 미설정 요소를 시각적으로 구분
---
## 8. 마이그레이션 전략
### 8.1 기존 데이터 처리
- 기존 `temp_material_master` 기반 배치 데이터를 수동 입력 모드로 전환
- `external_material_id``data_binding.mode = 'manual'`로 변환
### 8.2 단계적 전환
1. 새 스키마 적용 (기존 컬럼 유지)
2. 새 UI/로직 구현 및 테스트
3. 기존 데이터 마이그레이션
4. 임시 테이블 및 구 코드 제거
---
## 9. 기술 스택
- **백엔드**: PostgreSQL JSONB, Node.js/TypeScript
- **프론트엔드**: React, TypeScript, Shadcn UI
- **3D 렌더링**: React Three Fiber, Three.js
- **데이터 소스**: 기존 `DatabaseConfig`, `ExternalDbConfig`, `RestApiConfig` 컴포넌트 재사용
---
## 10. 예상 개발 기간
- Phase 1-2 (DB/백엔드): 1일
- Phase 3-4 (프론트엔드 구조): 1일
- Phase 5 (데이터 바인딩 모달): 2일
- Phase 6-7 (3D 렌더링/뷰어): 1일
- Phase 8 (정리 및 테스트): 0.5일
**총 예상 기간: 약 5.5일**

File diff suppressed because it is too large Load Diff

View File

@ -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"
}

View File

@ -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"
}

View File

@ -1,18 +0,0 @@
{
"id": "1d997eeb-3d61-427d-8b54-119d4372b9b3",
"sentAt": "2025-10-22T07:13:30.905Z",
"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;\">전달히야야양</p><p style=\"margin: 0 0 16px 0; line-height: 1.6;\"><br>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━<br>전달된 메일:</p><p style=\"margin: 0 0 16px 0; line-height: 1.6;\">보낸사람: \"이희진\" <zian9227@naver.com><br>날짜: 2025. 10. 22. 오후 12:58:15<br>제목: ㄴ<br>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</p><p style=\"margin: 0 0 16px 0; line-height: 1.6;\">ㄴㅇㄹㄴㅇㄹㄴㅇㄹ<br></p>\r\n </div>\r\n ",
"status": "success",
"messageId": "<d20cd501-04a4-bbe6-8b50-7f43e19bd70a@wace.me>",
"accepted": [
"zian9227@naver.com"
],
"rejected": []
}

View File

@ -1,29 +0,0 @@
{
"id": "1e492bb1-d069-4242-8cbf-9829b8f6c7e6",
"sentAt": "2025-10-13T01:08:34.764Z",
"accountId": "account-1759310844272",
"accountName": "이희진",
"accountEmail": "hjlee@wace.me",
"to": [
"zian9227@naver.com"
],
"subject": "제목 없음",
"htmlContent": "\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n</head>\n<body style=\"margin: 0; padding: 0; background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\">\n <table role=\"presentation\" style=\"width: 100%; border-collapse: collapse; background-color: #ffffff;\">\n <tr>\n <td style=\"padding: 20px;\">\n<div style=\"margin: 0 0 20px 0; color: #333; font-size: 15px; line-height: 1.6; text-align: left;\"><p>ㄴㅇㄹㄴㅇㄹ</p></div><div style=\"margin: 30px 0; text-align: left;\">\n <a href=\"https://example.com\" style=\"display: inline-block; padding: 14px 28px; background-color: #007bff; color: #fff; text-decoration: none; border-radius: 6px; font-weight: 600; font-size: 15px;\">ㄴㅇㄹ버튼</a>\n </div><div style=\"margin: 20px 0; text-align: left;\">\n <img src=\"https://placehold.co/600x200/e5e7eb/64748b?text=Image\" alt=\"\" style=\"max-width: 100%; height: auto; display: block; border-radius: 4px;\" />\n </div><div style=\"height: 20;\"></div><div style=\"margin: 0 0 20px 0; color: #333; font-size: 15px; line-height: 1.6; text-align: left;\"><p>ㄴㅇㄹ</p></div><div style=\"margin: 0 0 20px 0; color: #333; font-size: 15px; line-height: 1.6; text-align: left;\"><p>ㄴㅇㄹ</p></div>\n </td>\n </tr>\n </table>\n\n <div style=\"margin-top: 32px; padding-top: 24px; border-top: 1px solid #e5e7eb;\">\n \r\n <div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\">ㄴㅇㄹ</p>\r\n </div>\r\n \n </div>\n </body>\n</html>\n",
"templateId": "template-1760315158387",
"templateName": "테스트2",
"attachments": [
{
"filename": "스크린샷 2025-10-13 오전 10.00.06.png",
"originalName": "스크린샷 2025-10-13 오전 10.00.06.png",
"size": 0,
"path": "/app/uploads/mail-attachments/1760317712416-622369845.png",
"mimetype": "image/png"
}
],
"status": "success",
"messageId": "<f03bea59-9a77-b454-845e-7ad2a070bade@wace.me>",
"accepted": [
"zian9227@naver.com"
],
"rejected": []
}

View File

@ -1,41 +0,0 @@
{
"id": "2d848b19-26e1-45ad-8e2c-9205f1f01c87",
"sentAt": "2025-10-02T07:50:25.817Z",
"accountId": "account-1759310844272",
"accountName": "이희진",
"accountEmail": "hjlee@wace.me",
"to": [
"zian9227@naver.com"
],
"subject": "ㅣ;ㅏㅓ",
"htmlContent": "\r\n <div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\">ㅓㅏㅣ</p>\r\n </div>\r\n ",
"attachments": [
{
"filename": "test용 이미지33.jpg",
"originalName": "test용 이미지33.jpg",
"size": 0,
"path": "/app/uploads/mail-attachments/1759391422625-269479520_test____________________________33.jpg",
"mimetype": "image/jpeg"
},
{
"filename": "UI_개선사항_문서.md",
"originalName": "UI_개선사항_문서.md",
"size": 0,
"path": "/app/uploads/mail-attachments/1759391422626-68453569_UI_______________________________________________.md",
"mimetype": "text/x-markdown"
},
{
"filename": "test용 이미지2.png",
"originalName": "test용 이미지2.png",
"size": 0,
"path": "/app/uploads/mail-attachments/1759391422626-168170034_test____________________________2.png",
"mimetype": "image/png"
}
],
"status": "success",
"messageId": "<9d5b8275-e059-3a71-a34a-dea800730aa3@wace.me>",
"accepted": [
"zian9227@naver.com"
],
"rejected": []
}

View File

@ -1,18 +0,0 @@
{
"id": "331d95d6-3a13-4657-bc75-ab0811712eb8",
"sentAt": "2025-10-22T07:18:18.240Z",
"accountId": "account-1759310844272",
"accountName": "이희진",
"accountEmail": "hjlee@wace.me",
"to": [
"zian9227@naver.com"
],
"subject": "ㅁㄴㅇㄹㅁㄴㅇㄹ",
"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 ",
"status": "success",
"messageId": "<d4923c0d-f692-7d1d-d1b0-3b9e1e6cbab5@wace.me>",
"accepted": [
"zian9227@naver.com"
],
"rejected": []
}

File diff suppressed because one or more lines are too long

View File

@ -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"
}

View File

@ -1,41 +0,0 @@
{
"id": "37fce6a0-2301-431b-b573-82bdab9b8008",
"sentAt": "2025-10-02T07:44:38.128Z",
"accountId": "account-1759310844272",
"accountName": "이희진",
"accountEmail": "hjlee@wace.me",
"to": [
"zian9227@naver.com"
],
"subject": "asd",
"htmlContent": "\r\n <div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\">asd</p>\r\n </div>\r\n ",
"attachments": [
{
"filename": "웨이스-임직원-프로파일-이희진.key",
"originalName": "웨이스-임직원-프로파일-이희진.key",
"size": 0,
"path": "/app/uploads/mail-attachments/1759391076653-58189058___________________-___________________________-___________________________-_____________________.key",
"mimetype": "application/x-iwork-keynote-sffkey"
},
{
"filename": "웨이스-임직원-프로파일-이희진.pptx",
"originalName": "웨이스-임직원-프로파일-이희진.pptx",
"size": 0,
"path": "/app/uploads/mail-attachments/1759391076736-190208246___________________-___________________________-___________________________-_____________________.pptx",
"mimetype": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
},
{
"filename": "test용 이미지33.jpg",
"originalName": "test용 이미지33.jpg",
"size": 0,
"path": "/app/uploads/mail-attachments/1759391076738-240665795_test____________________________33.jpg",
"mimetype": "image/jpeg"
}
],
"status": "success",
"messageId": "<796cb9a7-df62-31c4-ae6b-b42f383d82b4@wace.me>",
"accepted": [
"zian9227@naver.com"
],
"rejected": []
}

View File

@ -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> \"이희진\" &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

@ -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"
}

View File

@ -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"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

File diff suppressed because one or more lines are too long

View File

@ -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"
}

View File

@ -1,18 +0,0 @@
{
"id": "84ee9619-49ff-4f61-a7fa-0bb0b0b7199a",
"sentAt": "2025-10-22T04:27:51.044Z",
"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": "<5fa451ff-7d29-7da4-ce56-ca7391c147af@wace.me>",
"accepted": [
"zian9227@naver.com"
],
"rejected": []
}

View File

@ -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"
}

View File

@ -1,18 +0,0 @@
{
"id": "89a32ace-f39b-44fa-b614-c65d96548f92",
"sentAt": "2025-10-22T03:49:48.461Z",
"accountId": "account-1759310844272",
"accountName": "이희진",
"accountEmail": "hjlee@wace.me",
"to": [
"zian9227@naver.com"
],
"subject": "Fwd: 기상청 API허브 회원가입 인증번호",
"htmlContent": "\r\n <div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\"><br> <br/><br/><br> <div style=\"border: 1px solid #ccc; padding: 15px; margin: 10px 0; background-color: #f9f9f9;\"><br> <p><strong>---------- 전달된 메시지 ----------</strong></p><br> <p><strong>보낸 사람:</strong> \"기상청 API허브\" <noreply@apihube.kma.go.kr></p><br> <p><strong>날짜:</strong> 2025. 10. 13. 오후 4:26:45</p><br> <p><strong>제목:</strong> 기상청 API허브 회원가입 인증번호</p><br> <hr style=\"border: none; border-top: 1px solid #ccc; margin: 10px 0;\" /><br> undefined<br> </div><br> </p>\r\n </div>\r\n ",
"status": "success",
"messageId": "<9b36ce56-4ef1-cf0c-1f39-2c73bcb521da@wace.me>",
"accepted": [
"zian9227@naver.com"
],
"rejected": []
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -1,29 +0,0 @@
{
"id": "9eab902e-f77b-424f-ada4-0ea8709b36bf",
"sentAt": "2025-10-13T00:53:55.193Z",
"accountId": "account-1759310844272",
"accountName": "이희진",
"accountEmail": "hjlee@wace.me",
"to": [
"zian9227@naver.com"
],
"subject": "제목 없음",
"htmlContent": "<div style=\"max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;\"><p style=\"margin: 16px 0; color: #333; font-size: 14px;\"><p>텍스트를 입력하세요...</p></p><div style=\"text-align: center; margin: 24px 0;\">\n <a href=\"https://example.com\" style=\"display: inline-block; padding: 12px 24px; background-color: #007bff; color: #fff; text-decoration: none; border-radius: 4px;\">버튼</a>\n </div><div style=\"text-align: center; margin: 16px 0;\">\n <img src=\"https://placehold.co/600x200/e5e7eb/64748b?text=Image\" alt=\"\" style=\"max-width: 100%; height: auto;\" />\n </div><div style=\"height: 20;\"></div><p style=\"margin: 16px 0; color: #333; font-size: 14px;\"><p>텍스트를 입력하세요...</p></p><p style=\"margin: 16px 0; color: #333; font-size: 14px;\"><p>텍스트를 입력하세요...</p></p>\n <div style=\"margin-top: 32px; padding-top: 24px; border-top: 1px solid #e5e7eb;\">\n \r\n <div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\">어덯게 나오는지 봅시다 추가메시지 영역이빈다.</p>\r\n </div>\r\n \n </div>\n </div>",
"templateId": "template-1760315158387",
"templateName": "테스트2",
"attachments": [
{
"filename": "한글.txt",
"originalName": "한글.txt",
"size": 0,
"path": "/app/uploads/mail-attachments/1760316833254-789302611.txt",
"mimetype": "text/plain"
}
],
"status": "success",
"messageId": "<3d0bef10-2e58-fd63-b175-c1f499af0102@wace.me>",
"accepted": [
"zian9227@naver.com"
],
"rejected": []
}

View File

@ -1,41 +0,0 @@
{
"id": "a1ca39ad-4467-44e0-963a-fba5037c8896",
"sentAt": "2025-10-02T08:22:14.721Z",
"accountId": "account-1759310844272",
"accountName": "이희진",
"accountEmail": "hjlee@wace.me",
"to": [
"zian9227@naver.com"
],
"subject": "ㅁㄴㅇㄹㅁㄴㅇㄹㅁㄴㅇㄹㅁㄴ",
"htmlContent": "\r\n <div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\">ㅁㄴㅇㄹㅁㄴㅇㄹㄴㅁㅇㄹ</p>\r\n </div>\r\n ",
"attachments": [
{
"filename": "test용 이미지33.jpg",
"originalName": "test용 이미지33.jpg",
"size": 0,
"path": "/app/uploads/mail-attachments/1759393332207-791945862_test____________________________33.jpg",
"mimetype": "image/jpeg"
},
{
"filename": "UI_개선사항_문서.md",
"originalName": "UI_개선사항_문서.md",
"size": 0,
"path": "/app/uploads/mail-attachments/1759393332208-660280542_UI_______________________________________________.md",
"mimetype": "text/x-markdown"
},
{
"filename": "test용 이미지2.png",
"originalName": "test용 이미지2.png",
"size": 0,
"path": "/app/uploads/mail-attachments/1759393332208-149486455_test____________________________2.png",
"mimetype": "image/png"
}
],
"status": "success",
"messageId": "<d52bab7c-4285-8a27-12ed-b501ff858d23@wace.me>",
"accepted": [
"zian9227@naver.com"
],
"rejected": []
}

View File

@ -1,41 +0,0 @@
{
"id": "a3a9aab1-4334-46bd-bf50-b867305f66c0",
"sentAt": "2025-10-02T08:41:42.086Z",
"accountId": "account-1759310844272",
"accountName": "이희진",
"accountEmail": "hjlee@wace.me",
"to": [
"zian9227@naver.com"
],
"subject": "한글테스트",
"htmlContent": "\r\n <div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\">ㅁㄴㅇㄹ</p>\r\n </div>\r\n ",
"attachments": [
{
"filename": "UI_개선사항_문서.md",
"originalName": "UI_개선사항_문서.md",
"size": 0,
"path": "/app/uploads/mail-attachments/1759394500462-50127394_UI_______________________________________________.md",
"mimetype": "text/x-markdown"
},
{
"filename": "test용 이미지33.jpg",
"originalName": "test용 이미지33.jpg",
"size": 0,
"path": "/app/uploads/mail-attachments/1759394500463-68744474_test____________________________33.jpg",
"mimetype": "image/jpeg"
},
{
"filename": "test용 이미지2.png",
"originalName": "test용 이미지2.png",
"size": 0,
"path": "/app/uploads/mail-attachments/1759394500463-464487722_test____________________________2.png",
"mimetype": "image/png"
}
],
"status": "success",
"messageId": "<2dbfbf64-69c2-a83d-6bb7-515e4e654628@wace.me>",
"accepted": [
"zian9227@naver.com"
],
"rejected": []
}

View File

@ -1,18 +0,0 @@
{
"id": "a638f7d0-ee31-47fa-9f72-de66ef31ea44",
"sentAt": "2025-10-22T07:21:13.723Z",
"accountId": "account-1759310844272",
"accountName": "이희진",
"accountEmail": "hjlee@wace.me",
"to": [
"zian9227@naver.com"
],
"subject": "ㄹㅇㄴㅁㄹㅇㄴㅁ",
"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 ",
"status": "success",
"messageId": "<5ea07d02-78bf-a655-8289-bcbd8eaf7741@wace.me>",
"accepted": [
"zian9227@naver.com"
],
"rejected": []
}

View File

@ -1,48 +0,0 @@
{
"id": "b1d8f458-076c-4c44-982e-d2f46dcd4b03",
"sentAt": "2025-10-02T08:57:48.412Z",
"accountId": "account-1759310844272",
"accountName": "이희진",
"accountEmail": "hjlee@wace.me",
"to": [
"zian9227@naver.com"
],
"subject": "ㅁㄴㅇㄹ",
"htmlContent": "\r\n <div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\">ㅁㄴㅇㄹ</p>\r\n </div>\r\n ",
"attachments": [
{
"filename": "웨이스-임직원-프로파일-이희진.key",
"originalName": "웨이스-임직원-프로파일-이희진.key",
"size": 0,
"path": "/app/uploads/mail-attachments/1759395465488-120933172.key",
"mimetype": "application/x-iwork-keynote-sffkey"
},
{
"filename": "UI_개선사항_문서.md",
"originalName": "UI_개선사항_문서.md",
"size": 0,
"path": "/app/uploads/mail-attachments/1759395465566-306126854.md",
"mimetype": "text/x-markdown"
},
{
"filename": "test용 이미지33.jpg",
"originalName": "test용 이미지33.jpg",
"size": 0,
"path": "/app/uploads/mail-attachments/1759395465566-412984398.jpg",
"mimetype": "image/jpeg"
},
{
"filename": "test용 이미지2.png",
"originalName": "test용 이미지2.png",
"size": 0,
"path": "/app/uploads/mail-attachments/1759395465567-143883587.png",
"mimetype": "image/png"
}
],
"status": "success",
"messageId": "<e2796753-a1a9-fbac-c035-00341e29031c@wace.me>",
"accepted": [
"zian9227@naver.com"
],
"rejected": []
}

View File

@ -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"
}

View File

@ -1,41 +0,0 @@
{
"id": "b75d0b2b-7d8a-461b-b854-2bebdef959e8",
"sentAt": "2025-10-02T08:49:30.356Z",
"accountId": "account-1759310844272",
"accountName": "이희진",
"accountEmail": "hjlee@wace.me",
"to": [
"zian9227@naver.com"
],
"subject": "한글2",
"htmlContent": "\r\n <div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\">ㅁㄴㅇㄹ</p>\r\n </div>\r\n ",
"attachments": [
{
"filename": "UI_áá¢áá¥á«áá¡áá¡á¼_áá®á«áá¥.md",
"originalName": "UI_áá¢áá¥á«áá¡áá¡á¼_áá®á«áá¥.md",
"size": 0,
"path": "/app/uploads/mail-attachments/1759394969516-74008147_UI__________________________.md",
"mimetype": "text/x-markdown"
},
{
"filename": "testáá­á¼ ááµááµááµ33.jpg",
"originalName": "testáá­á¼ ááµááµááµ33.jpg",
"size": 0,
"path": "/app/uploads/mail-attachments/1759394969516-530544653_test_______________33.jpg",
"mimetype": "image/jpeg"
},
{
"filename": "testáá­á¼ ááµááµááµ2.png",
"originalName": "testáá­á¼ ááµááµááµ2.png",
"size": 0,
"path": "/app/uploads/mail-attachments/1759394969517-260831218_test_______________2.png",
"mimetype": "image/png"
}
],
"status": "success",
"messageId": "<80a431a1-bb4d-31b5-2564-93f8c2539fd4@wace.me>",
"accepted": [
"zian9227@naver.com"
],
"rejected": []
}

View File

@ -1,41 +0,0 @@
{
"id": "ccdd8961-1b3f-4b88-b838-51d6ed8f1601",
"sentAt": "2025-10-02T08:47:03.481Z",
"accountId": "account-1759310844272",
"accountName": "이희진",
"accountEmail": "hjlee@wace.me",
"to": [
"zian9227@naver.com"
],
"subject": "한글테스트222",
"htmlContent": "\r\n <div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\">2</p>\r\n </div>\r\n ",
"attachments": [
{
"filename": "UI_áá¢áá¥á«áá¡áá¡á¼_áá®á«áá¥.md",
"originalName": "UI_áá¢áá¥á«áá¡áá¡á¼_áá®á«áá¥.md",
"size": 0,
"path": "/app/uploads/mail-attachments/1759394821751-229305880_UI__________________________.md",
"mimetype": "text/x-markdown"
},
{
"filename": "testáá­á¼ ááµááµááµ33.jpg",
"originalName": "testáá­á¼ ááµááµááµ33.jpg",
"size": 0,
"path": "/app/uploads/mail-attachments/1759394821751-335146895_test_______________33.jpg",
"mimetype": "image/jpeg"
},
{
"filename": "testáá­á¼ ááµááµááµ2.png",
"originalName": "testáá­á¼ ááµááµááµ2.png",
"size": 0,
"path": "/app/uploads/mail-attachments/1759394821753-911076131_test_______________2.png",
"mimetype": "image/png"
}
],
"status": "success",
"messageId": "<69519c70-a5cd-421d-9976-8c7014d69b39@wace.me>",
"accepted": [
"zian9227@naver.com"
],
"rejected": []
}

View File

@ -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"
}

File diff suppressed because one or more lines are too long

View File

@ -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"
}

View File

@ -1,27 +0,0 @@
{
"id": "e93848a8-6901-44c4-b4db-27c8d2aeb8dd",
"sentAt": "2025-10-22T04:28:42.686Z",
"accountId": "account-1759310844272",
"accountName": "이희진",
"accountEmail": "hjlee@wace.me",
"to": [
"\"권은아\" <chna8137s@gmail.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;\">호홋 답장 기능을 구현했다죵<br>얼른 퇴근하고 싪네여</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> \"권은아\" <chna8137s@gmail.com></p>\r\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 1:10:37</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/1761107318152-717716316.png",
"mimetype": "image/png"
}
],
"status": "success",
"messageId": "<19981423-259b-0a50-e76d-23c860692c16@wace.me>",
"accepted": [
"chna8137s@gmail.com"
],
"rejected": []
}

View File

@ -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"
}

View File

@ -1,41 +0,0 @@
{
"id": "ee0d162c-48ad-4c00-8c56-ade80be4503f",
"sentAt": "2025-10-02T08:48:29.740Z",
"accountId": "account-1759310844272",
"accountName": "이희진",
"accountEmail": "hjlee@wace.me",
"to": [
"zian9227@naver.com"
],
"subject": "한글한글",
"htmlContent": "\r\n <div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\">ㅁㄴㅇㄹㅁㄴㅇㄹㅁㄴㅇㄹ</p>\r\n </div>\r\n ",
"attachments": [
{
"filename": "UI_áá¢áá¥á«áá¡áá¡á¼_áá®á«áá¥.md",
"originalName": "UI_áá¢áá¥á«áá¡áá¡á¼_áá®á«áá¥.md",
"size": 0,
"path": "/app/uploads/mail-attachments/1759394908877-38147683_UI__________________________.md",
"mimetype": "text/x-markdown"
},
{
"filename": "testáá­á¼ ááµááµááµ33.jpg",
"originalName": "testáá­á¼ ááµááµááµ33.jpg",
"size": 0,
"path": "/app/uploads/mail-attachments/1759394908879-80461065_test_______________33.jpg",
"mimetype": "image/jpeg"
},
{
"filename": "testáá­á¼ ááµááµááµ2.png",
"originalName": "testáá­á¼ ááµááµááµ2.png",
"size": 0,
"path": "/app/uploads/mail-attachments/1759394908880-475630926_test_______________2.png",
"mimetype": "image/png"
}
],
"status": "success",
"messageId": "<96205714-1a6b-adb7-7ae5-0e1e3fcb700b@wace.me>",
"accepted": [
"zian9227@naver.com"
],
"rejected": []
}

View File

@ -1,29 +0,0 @@
{
"id": "fc26aba3-6b6e-47ba-91e8-609ae25e0e7d",
"sentAt": "2025-10-13T00:21:51.799Z",
"accountId": "account-1759310844272",
"accountName": "이희진",
"accountEmail": "hjlee@wace.me",
"to": [
"zian9227@naver.com"
],
"subject": "test용입니다.",
"htmlContent": "\r\n <div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; color: #333;\">\r\n <p style=\"margin: 0 0 16px 0; line-height: 1.6;\">ㅁㄴㅇㄹ</p>\r\n </div>\r\n ",
"templateId": "template-1759302346758",
"templateName": "test",
"attachments": [
{
"filename": "웨이스-임직원-프로파일-이희진.key",
"originalName": "웨이스-임직원-프로파일-이희진.key",
"size": 0,
"path": "/app/uploads/mail-attachments/1760314910154-84512253.key",
"mimetype": "application/x-iwork-keynote-sffkey"
}
],
"status": "success",
"messageId": "<c84bcecc-2e8f-4a32-1b7f-44a91b195b2d@wace.me>",
"accepted": [
"zian9227@naver.com"
],
"rejected": []
}

View File

@ -1,18 +0,0 @@
{
"id": "fcea6149-a098-4212-aa00-baef0cc083d6",
"sentAt": "2025-10-22T04:24:54.126Z",
"accountId": "account-1759310844272",
"accountName": "이희진",
"accountEmail": "hjlee@wace.me",
"to": [
"\"DHS\" <ddhhss0603@gmail.com>"
],
"subject": "Re: 안녕하세여",
"htmlContent": "\r\n <div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; 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> \"DHS\" <ddhhss0603@gmail.com></p>\r\n <p><strong>날짜:</strong> 2025. 10. 22. 오후 1:09:49</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": "<c24b04f0-b958-5e0b-4cc7-2bff30f23c2c@wace.me>",
"accepted": [
"ddhhss0603@gmail.com"
],
"rejected": []
}

View File

@ -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"
}

View File

@ -12,12 +12,15 @@
"@types/mssql": "^9.1.8", "@types/mssql": "^9.1.8",
"axios": "^1.11.0", "axios": "^1.11.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"bwip-js": "^4.8.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"cors": "^2.8.5", "cors": "^2.8.5",
"docx": "^9.5.1",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"express-rate-limit": "^7.1.5", "express-rate-limit": "^7.1.5",
"helmet": "^7.1.0", "helmet": "^7.1.0",
"html-to-docx": "^1.8.0",
"iconv-lite": "^0.7.0", "iconv-lite": "^0.7.0",
"imap": "^0.8.19", "imap": "^0.8.19",
"joi": "^17.11.0", "joi": "^17.11.0",
@ -39,6 +42,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/bwip-js": "^3.2.3",
"@types/compression": "^1.7.5", "@types/compression": "^1.7.5",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
@ -1040,6 +1044,7 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3", "@babel/generator": "^7.28.3",
@ -2256,6 +2261,93 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@oozcitak/dom": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.6.tgz",
"integrity": "sha512-k4uEIa6DI3FCrFJMGq/05U/59WnS9DjME0kaPqBRCJAqBTkmopbYV1Xs4qFKbDJ/9wOg8W97p+1E0heng/LH7g==",
"license": "MIT",
"dependencies": {
"@oozcitak/infra": "1.0.5",
"@oozcitak/url": "1.0.0",
"@oozcitak/util": "8.3.4"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/@oozcitak/infra": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.5.tgz",
"integrity": "sha512-o+zZH7M6l5e3FaAWy3ojaPIVN5eusaYPrKm6MZQt0DKNdgXa2wDYExjpP0t/zx+GoQgQKzLu7cfD8rHCLt8JrQ==",
"license": "MIT",
"dependencies": {
"@oozcitak/util": "8.0.0"
},
"engines": {
"node": ">=6.0"
}
},
"node_modules/@oozcitak/infra/node_modules/@oozcitak/util": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.0.0.tgz",
"integrity": "sha512-+9Hq6yuoq/3TRV/n/xcpydGBq2qN2/DEDMqNTG7rm95K6ZE2/YY/sPyx62+1n8QsE9O26e5M1URlXsk+AnN9Jw==",
"license": "MIT",
"engines": {
"node": ">=6.0"
}
},
"node_modules/@oozcitak/url": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.0.tgz",
"integrity": "sha512-LGrMeSxeLzsdaitxq3ZmBRVOrlRRQIgNNci6L0VRnOKlJFuRIkNm4B+BObXPCJA6JT5bEJtrrwjn30jueHJYZQ==",
"license": "MIT",
"dependencies": {
"@oozcitak/infra": "1.0.3",
"@oozcitak/util": "1.0.2"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/@oozcitak/url/node_modules/@oozcitak/infra": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.3.tgz",
"integrity": "sha512-9O2wxXGnRzy76O1XUxESxDGsXT5kzETJPvYbreO4mv6bqe1+YSuux2cZTagjJ/T4UfEwFJz5ixanOqB0QgYAag==",
"license": "MIT",
"dependencies": {
"@oozcitak/util": "1.0.1"
},
"engines": {
"node": ">=6.0"
}
},
"node_modules/@oozcitak/url/node_modules/@oozcitak/infra/node_modules/@oozcitak/util": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-1.0.1.tgz",
"integrity": "sha512-dFwFqcKrQnJ2SapOmRD1nQWEZUtbtIy9Y6TyJquzsalWNJsKIPxmTI0KG6Ypyl8j7v89L2wixH9fQDNrF78hKg==",
"license": "MIT",
"engines": {
"node": ">=6.0"
}
},
"node_modules/@oozcitak/url/node_modules/@oozcitak/util": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-1.0.2.tgz",
"integrity": "sha512-4n8B1cWlJleSOSba5gxsMcN4tO8KkkcvXhNWW+ADqvq9Xj+Lrl9uCa90GRpjekqQJyt84aUX015DG81LFpZYXA==",
"license": "MIT",
"engines": {
"node": ">=6.0"
}
},
"node_modules/@oozcitak/util": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.4.tgz",
"integrity": "sha512-6gH/bLQJSJEg7OEpkH4wGQdA8KXHRbzL1YkGyUO12YNAgV3jxKy4K9kvfXj4+9T0OLug5k58cnPCKSSIKzp7pg==",
"license": "MIT",
"engines": {
"node": ">=8.0"
}
},
"node_modules/@paralleldrive/cuid2": { "node_modules/@paralleldrive/cuid2": {
"version": "2.2.2", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
@ -2280,6 +2372,7 @@
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"cluster-key-slot": "1.1.2", "cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0", "generic-pool": "3.9.0",
@ -3124,6 +3217,16 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/bwip-js": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@types/bwip-js/-/bwip-js-3.2.3.tgz",
"integrity": "sha512-kgL1GOW7n5FhlC5aXnckaEim0rz1cFM4t9/xUwuNXCIDnWLx8ruQ4JQkG6znq4GQFovNLhQy5JdgbDwJw4D/zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/compression": { "node_modules/@types/compression": {
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz",
@ -3373,6 +3476,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz",
"integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
@ -3609,6 +3713,7 @@
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/scope-manager": "6.21.0",
"@typescript-eslint/types": "6.21.0", "@typescript-eslint/types": "6.21.0",
@ -3826,6 +3931,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -4326,6 +4432,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/browser-split": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/browser-split/-/browser-split-0.0.1.tgz",
"integrity": "sha512-JhvgRb2ihQhsljNda3BI8/UcRHVzrVwo3Q+P8vDtSiyobXuFpuZ9mq+MbRGMnC22CjW3RrfXdg6j6ITX8M+7Ow==",
"license": "MIT"
},
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.26.2", "version": "4.26.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz",
@ -4346,6 +4458,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.3", "baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741", "caniuse-lite": "^1.0.30001741",
@ -4445,6 +4558,15 @@
"node": ">=10.16.0" "node": ">=10.16.0"
} }
}, },
"node_modules/bwip-js": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/bwip-js/-/bwip-js-4.8.0.tgz",
"integrity": "sha512-gUDkDHSTv8/DJhomSIbO0fX/Dx0MO/sgllLxJyJfu4WixCQe9nfGJzmHm64ZCbxo+gUYQEsQcRmqcwcwPRwUkg==",
"license": "MIT",
"bin": {
"bwip-js": "bin/bwip-js.js"
}
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -4521,6 +4643,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/camelize": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001745", "version": "1.0.30001745",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz",
@ -5202,6 +5333,56 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/docx": {
"version": "9.5.1",
"resolved": "https://registry.npmjs.org/docx/-/docx-9.5.1.tgz",
"integrity": "sha512-ABDI7JEirFD2+bHhOBlsGZxaG1UgZb2M/QMKhLSDGgVNhxDesTCDcP+qoDnDGjZ4EOXTRfUjUgwHVuZ6VSTfWQ==",
"license": "MIT",
"dependencies": {
"@types/node": "^24.0.1",
"hash.js": "^1.1.7",
"jszip": "^3.10.1",
"nanoid": "^5.1.3",
"xml": "^1.0.1",
"xml-js": "^1.6.8"
},
"engines": {
"node": ">=10"
}
},
"node_modules/docx/node_modules/@types/node": {
"version": "24.10.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz",
"integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/docx/node_modules/nanoid": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
"integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/docx/node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"license": "MIT"
},
"node_modules/dom-serializer": { "node_modules/dom-serializer": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
@ -5216,6 +5397,11 @@
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
} }
}, },
"node_modules/dom-walk": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
},
"node_modules/domelementtype": { "node_modules/domelementtype": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
@ -5349,6 +5535,27 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/ent": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz",
"integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.3",
"es-errors": "^1.3.0",
"punycode": "^1.4.1",
"safe-regex-test": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ent/node_modules/punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
"license": "MIT"
},
"node_modules/entities": { "node_modules/entities": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@ -5361,6 +5568,16 @@
"url": "https://github.com/fb55/entities?sponsor=1" "url": "https://github.com/fb55/entities?sponsor=1"
} }
}, },
"node_modules/error": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/error/-/error-4.4.0.tgz",
"integrity": "sha512-SNDKualLUtT4StGFP7xNfuFybL2f6iJujFtrWuvJqGbVQGaN+adE23veqzPz1hjUjTunLi2EnJ+0SJxtbJreKw==",
"dependencies": {
"camelize": "^1.0.0",
"string-template": "~0.2.0",
"xtend": "~4.0.0"
}
},
"node_modules/error-ex": { "node_modules/error-ex": {
"version": "1.3.4", "version": "1.3.4",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
@ -5452,6 +5669,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1", "@eslint-community/regexpp": "^4.6.1",
@ -5643,6 +5861,14 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/ev-store": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/ev-store/-/ev-store-7.0.0.tgz",
"integrity": "sha512-otazchNRnGzp2YarBJ+GXKVGvhxVATB1zmaStxJBYet0Dyq7A9VhH8IUEB/gRcL6Ch52lfpgPTRJ2m49epyMsQ==",
"dependencies": {
"individual": "^3.0.0"
}
},
"node_modules/event-target-shim": { "node_modules/event-target-shim": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
@ -6279,6 +6505,16 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/global": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
"license": "MIT",
"dependencies": {
"min-document": "^2.19.0",
"process": "^0.11.10"
}
},
"node_modules/globals": { "node_modules/globals": {
"version": "13.24.0", "version": "13.24.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
@ -6413,6 +6649,16 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@ -6443,6 +6689,22 @@
"node": ">=16.0.0" "node": ">=16.0.0"
} }
}, },
"node_modules/html-entities": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
"integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/mdevils"
},
{
"type": "patreon",
"url": "https://patreon.com/mdevils"
}
],
"license": "MIT"
},
"node_modules/html-escaper": { "node_modules/html-escaper": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@ -6450,6 +6712,27 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/html-to-docx": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/html-to-docx/-/html-to-docx-1.8.0.tgz",
"integrity": "sha512-IiMBWIqXM4+cEsW//RKoonWV7DlXAJBmmKI73XJSVWTIXjGUaxSr2ck1jqzVRZknpvO8xsFnVicldKVAWrBYBA==",
"license": "MIT",
"dependencies": {
"@oozcitak/dom": "1.15.6",
"@oozcitak/util": "8.3.4",
"color-name": "^1.1.4",
"html-entities": "^2.3.3",
"html-to-vdom": "^0.7.0",
"image-size": "^1.0.0",
"image-to-base64": "^2.2.0",
"jszip": "^3.7.1",
"lodash": "^4.17.21",
"mime-types": "^2.1.35",
"nanoid": "^3.1.25",
"virtual-dom": "^2.1.1",
"xmlbuilder2": "2.1.2"
}
},
"node_modules/html-to-text": { "node_modules/html-to-text": {
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
@ -6466,6 +6749,106 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/html-to-vdom": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/html-to-vdom/-/html-to-vdom-0.7.0.tgz",
"integrity": "sha512-k+d2qNkbx0JO00KezQsNcn6k2I/xSBP4yXYFLvXbcasTTDh+RDLUJS3puxqyNnpdyXWRHFGoKU7cRmby8/APcQ==",
"license": "ISC",
"dependencies": {
"ent": "^2.0.0",
"htmlparser2": "^3.8.2"
}
},
"node_modules/html-to-vdom/node_modules/dom-serializer": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
"integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.0.1",
"entities": "^2.0.0"
}
},
"node_modules/html-to-vdom/node_modules/dom-serializer/node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/html-to-vdom/node_modules/dom-serializer/node_modules/entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
"license": "BSD-2-Clause",
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/html-to-vdom/node_modules/domelementtype": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
"license": "BSD-2-Clause"
},
"node_modules/html-to-vdom/node_modules/domhandler": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
"integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "1"
}
},
"node_modules/html-to-vdom/node_modules/domutils": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
"integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "0",
"domelementtype": "1"
}
},
"node_modules/html-to-vdom/node_modules/entities": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
"license": "BSD-2-Clause"
},
"node_modules/html-to-vdom/node_modules/htmlparser2": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
"integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
"license": "MIT",
"dependencies": {
"domelementtype": "^1.3.1",
"domhandler": "^2.3.0",
"domutils": "^1.5.1",
"entities": "^1.1.1",
"inherits": "^2.0.1",
"readable-stream": "^3.1.1"
}
},
"node_modules/html-to-vdom/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/htmlparser2": { "node_modules/htmlparser2": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
@ -6590,6 +6973,30 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/image-size": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
"integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
"license": "MIT",
"dependencies": {
"queue": "6.0.2"
},
"bin": {
"image-size": "bin/image-size.js"
},
"engines": {
"node": ">=16.x"
}
},
"node_modules/image-to-base64": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/image-to-base64/-/image-to-base64-2.2.0.tgz",
"integrity": "sha512-Z+aMwm/91UOQqHhrz7Upre2ytKhWejZlWV/JxUTD1sT7GWWKFDJUEV5scVQKnkzSgPHFuQBUEWcanO+ma0PSVw==",
"license": "MIT",
"dependencies": {
"node-fetch": "^2.6.0"
}
},
"node_modules/imap": { "node_modules/imap": {
"version": "0.8.19", "version": "0.8.19",
"resolved": "https://registry.npmjs.org/imap/-/imap-0.8.19.tgz", "resolved": "https://registry.npmjs.org/imap/-/imap-0.8.19.tgz",
@ -6626,6 +7033,12 @@
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@ -6673,6 +7086,11 @@
"node": ">=0.8.19" "node": ">=0.8.19"
} }
}, },
"node_modules/individual": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/individual/-/individual-3.0.0.tgz",
"integrity": "sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g=="
},
"node_modules/inflight": { "node_modules/inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -6854,6 +7272,15 @@
"node": ">=0.12.0" "node": ">=0.12.0"
} }
}, },
"node_modules/is-object": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz",
"integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-path-inside": { "node_modules/is-path-inside": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
@ -7005,6 +7432,7 @@
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@jest/core": "^29.7.0", "@jest/core": "^29.7.0",
"@jest/types": "^29.6.3", "@jest/types": "^29.6.3",
@ -7696,6 +8124,18 @@
"npm": ">=6" "npm": ">=6"
} }
}, },
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/jwa": { "node_modules/jwa": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
@ -7812,6 +8252,15 @@
"integrity": "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==", "integrity": "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lines-and-columns": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@ -7953,7 +8402,6 @@
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0" "js-tokens": "^3.0.0 || ^4.0.0"
}, },
@ -8177,6 +8625,21 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/min-document": {
"version": "2.19.2",
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz",
"integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==",
"license": "MIT",
"dependencies": {
"dom-walk": "^0.1.0"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"license": "ISC"
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "9.0.3", "version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
@ -8300,6 +8763,24 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/native-duplexpair": { "node_modules/native-duplexpair": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz", "resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz",
@ -8329,6 +8810,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/next-tick": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-0.2.2.tgz",
"integrity": "sha512-f7h4svPtl+QidoBv4taKXUjJ70G2asaZ8G28nS0OkqaalX8dwwrtWtyxEDPK62AC00ur/+/E0pUwBwY5EPn15Q==",
"license": "MIT"
},
"node_modules/node-cron": { "node_modules/node-cron": {
"version": "4.2.1", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
@ -8670,6 +9157,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parchment": { "node_modules/parchment": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz", "resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz",
@ -8797,6 +9290,7 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"pg-connection-string": "^2.9.1", "pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1", "pg-pool": "^3.10.1",
@ -9179,6 +9673,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
"license": "MIT",
"dependencies": {
"inherits": "~2.0.3"
}
},
"node_modules/queue-microtask": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -9595,6 +10098,23 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/safe-regex-test": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"is-regex": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-stable-stringify": { "node_modules/safe-stable-stringify": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
@ -9610,12 +10130,17 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/sax": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
"integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==",
"license": "BlueOak-1.0.0"
},
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.23.2", "version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
} }
@ -9744,6 +10269,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/setprototypeof": { "node_modules/setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@ -10020,6 +10551,11 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/string-template": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz",
"integrity": "sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw=="
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@ -10413,6 +10949,7 @@
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@cspotcode/source-map-support": "^0.8.0", "@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7", "@tsconfig/node10": "^1.0.7",
@ -10518,6 +11055,7 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -10685,6 +11223,22 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/virtual-dom": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/virtual-dom/-/virtual-dom-2.1.1.tgz",
"integrity": "sha512-wb6Qc9Lbqug0kRqo/iuApfBpJJAq14Sk1faAnSmtqXiwahg7PVTvWMs9L02Z8nNIMqbwsxzBAA90bbtRLbw0zg==",
"license": "MIT",
"dependencies": {
"browser-split": "0.0.1",
"error": "^4.3.0",
"ev-store": "^7.0.0",
"global": "^4.3.0",
"is-object": "^1.0.1",
"next-tick": "^0.2.2",
"x-is-array": "0.1.0",
"x-is-string": "0.1.0"
}
},
"node_modules/walker": { "node_modules/walker": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
@ -10862,6 +11416,80 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/x-is-array": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/x-is-array/-/x-is-array-0.1.0.tgz",
"integrity": "sha512-goHPif61oNrr0jJgsXRfc8oqtYzvfiMJpTqwE7Z4y9uH+T3UozkGqQ4d2nX9mB9khvA8U2o/UbPOFjgC7hLWIA=="
},
"node_modules/x-is-string": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz",
"integrity": "sha512-GojqklwG8gpzOVEVki5KudKNoq7MbbjYZCbyWzEz7tyPA7eleiE0+ePwOWQQRb5fm86rD3S8Tc0tSFf3AOv50w=="
},
"node_modules/xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
"license": "MIT"
},
"node_modules/xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"license": "MIT",
"dependencies": {
"sax": "^1.2.4"
},
"bin": {
"xml-js": "bin/cli.js"
}
},
"node_modules/xmlbuilder2": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-2.1.2.tgz",
"integrity": "sha512-PI710tmtVlQ5VmwzbRTuhmVhKnj9pM8Si+iOZCV2g2SNo3gCrpzR2Ka9wNzZtqfD+mnP+xkrqoNy0sjKZqP4Dg==",
"license": "MIT",
"dependencies": {
"@oozcitak/dom": "1.15.5",
"@oozcitak/infra": "1.0.5",
"@oozcitak/util": "8.3.3"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/xmlbuilder2/node_modules/@oozcitak/dom": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.5.tgz",
"integrity": "sha512-L6v3Mwb0TaYBYgeYlIeBaHnc+2ZEaDSbFiRm5KmqZQSoBlbPlf+l6aIH/sD5GUf2MYwULw00LT7+dOnEuAEC0A==",
"license": "MIT",
"dependencies": {
"@oozcitak/infra": "1.0.5",
"@oozcitak/url": "1.0.0",
"@oozcitak/util": "8.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/xmlbuilder2/node_modules/@oozcitak/dom/node_modules/@oozcitak/util": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.0.0.tgz",
"integrity": "sha512-+9Hq6yuoq/3TRV/n/xcpydGBq2qN2/DEDMqNTG7rm95K6ZE2/YY/sPyx62+1n8QsE9O26e5M1URlXsk+AnN9Jw==",
"license": "MIT",
"engines": {
"node": ">=6.0"
}
},
"node_modules/xmlbuilder2/node_modules/@oozcitak/util": {
"version": "8.3.3",
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.3.tgz",
"integrity": "sha512-Ufpab7G5PfnEhQyy5kDg9C8ltWJjsVT1P/IYqacjstaqydG4Q21HAT2HUZQYBrC/a1ZLKCz87pfydlDvv8y97w==",
"license": "MIT",
"engines": {
"node": ">=6.0"
}
},
"node_modules/xtend": { "node_modules/xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@ -26,12 +26,15 @@
"@types/mssql": "^9.1.8", "@types/mssql": "^9.1.8",
"axios": "^1.11.0", "axios": "^1.11.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"bwip-js": "^4.8.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"cors": "^2.8.5", "cors": "^2.8.5",
"docx": "^9.5.1",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"express-rate-limit": "^7.1.5", "express-rate-limit": "^7.1.5",
"helmet": "^7.1.0", "helmet": "^7.1.0",
"html-to-docx": "^1.8.0",
"iconv-lite": "^0.7.0", "iconv-lite": "^0.7.0",
"imap": "^0.8.19", "imap": "^0.8.19",
"joi": "^17.11.0", "joi": "^17.11.0",
@ -53,6 +56,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/bwip-js": "^3.2.3",
"@types/compression": "^1.7.5", "@types/compression": "^1.7.5",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",

View File

@ -0,0 +1,209 @@
/**
* DB (DO_DY)
* READ-ONLY: SELECT
*/
import { Pool } from "pg";
import mysql from "mysql2/promise";
import { CredentialEncryption } from "../src/utils/credentialEncryption";
async function testDigitalTwinDb() {
// 내부 DB 연결 (연결 정보 저장용)
const internalPool = new Pool({
host: process.env.DB_HOST || "localhost",
port: parseInt(process.env.DB_PORT || "5432"),
database: process.env.DB_NAME || "plm",
user: process.env.DB_USER || "postgres",
password: process.env.DB_PASSWORD || "ph0909!!",
});
const encryptionKey =
process.env.ENCRYPTION_SECRET_KEY || "default-secret-key-for-development";
const encryption = new CredentialEncryption(encryptionKey);
try {
console.log("🚀 디지털 트윈 외부 DB 연결 테스트 시작\n");
// 디지털 트윈 외부 DB 연결 정보
const digitalTwinConnection = {
name: "디지털트윈_DO_DY",
description: "디지털 트윈 후판(자재) 재고 정보 데이터베이스 (MariaDB)",
dbType: "mysql", // MariaDB는 MySQL 프로토콜 사용
host: "1.240.13.83",
port: 4307,
databaseName: "DO_DY",
username: "root",
password: "pohangms619!#",
sslEnabled: false,
isActive: true,
};
console.log("📝 연결 정보:");
console.log(` - 이름: ${digitalTwinConnection.name}`);
console.log(` - DB 타입: ${digitalTwinConnection.dbType}`);
console.log(` - 호스트: ${digitalTwinConnection.host}:${digitalTwinConnection.port}`);
console.log(` - 데이터베이스: ${digitalTwinConnection.databaseName}\n`);
// 1. 외부 DB 직접 연결 테스트
console.log("🔍 외부 DB 직접 연결 테스트 중...");
const externalConnection = await mysql.createConnection({
host: digitalTwinConnection.host,
port: digitalTwinConnection.port,
database: digitalTwinConnection.databaseName,
user: digitalTwinConnection.username,
password: digitalTwinConnection.password,
connectTimeout: 10000,
});
console.log("✅ 외부 DB 연결 성공!\n");
// 2. SELECT 쿼리 실행
console.log("📊 WSTKKY 테이블 쿼리 실행 중...\n");
const query = `
SELECT
SKUMKEY --
, SKUDESC --
, SKUTHIC --
, SKUWIDT --
, SKULENG --
, SKUWEIG --
, STOTQTY --
, SUOMKEY --
FROM DO_DY.WSTKKY
LIMIT 10
`;
const [rows] = await externalConnection.execute(query);
console.log("✅ 쿼리 실행 성공!\n");
console.log(`📦 조회된 데이터: ${Array.isArray(rows) ? rows.length : 0}\n`);
if (Array.isArray(rows) && rows.length > 0) {
console.log("🔍 샘플 데이터 (첫 3건):\n");
rows.slice(0, 3).forEach((row: any, index: number) => {
console.log(`[${index + 1}]`);
console.log(` 제품번호(SKUMKEY): ${row.SKUMKEY}`);
console.log(` 자재명(SKUDESC): ${row.SKUDESC}`);
console.log(` 두께(SKUTHIC): ${row.SKUTHIC}`);
console.log(` 폭(SKUWIDT): ${row.SKUWIDT}`);
console.log(` 길이(SKULENG): ${row.SKULENG}`);
console.log(` 중량(SKUWEIG): ${row.SKUWEIG}`);
console.log(` 수량(STOTQTY): ${row.STOTQTY}`);
console.log(` 단위(SUOMKEY): ${row.SUOMKEY}\n`);
});
// 전체 데이터 JSON 출력
console.log("📄 전체 데이터 (JSON):");
console.log(JSON.stringify(rows, null, 2));
console.log("\n");
}
await externalConnection.end();
// 3. 내부 DB에 연결 정보 저장
console.log("💾 내부 DB에 연결 정보 저장 중...");
const encryptedPassword = encryption.encrypt(digitalTwinConnection.password);
// 중복 체크
const existingResult = await internalPool.query(
"SELECT id FROM flow_external_db_connection WHERE name = $1",
[digitalTwinConnection.name]
);
let connectionId: number;
if (existingResult.rows.length > 0) {
connectionId = existingResult.rows[0].id;
console.log(`⚠️ 이미 존재하는 연결 (ID: ${connectionId})`);
// 기존 연결 업데이트
await internalPool.query(
`UPDATE flow_external_db_connection
SET description = $1,
db_type = $2,
host = $3,
port = $4,
database_name = $5,
username = $6,
password_encrypted = $7,
ssl_enabled = $8,
is_active = $9,
updated_at = NOW(),
updated_by = 'system'
WHERE name = $10`,
[
digitalTwinConnection.description,
digitalTwinConnection.dbType,
digitalTwinConnection.host,
digitalTwinConnection.port,
digitalTwinConnection.databaseName,
digitalTwinConnection.username,
encryptedPassword,
digitalTwinConnection.sslEnabled,
digitalTwinConnection.isActive,
digitalTwinConnection.name,
]
);
console.log(`✅ 연결 정보 업데이트 완료`);
} else {
// 새 연결 추가
const result = await internalPool.query(
`INSERT INTO flow_external_db_connection (
name,
description,
db_type,
host,
port,
database_name,
username,
password_encrypted,
ssl_enabled,
is_active,
created_by
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'system')
RETURNING id`,
[
digitalTwinConnection.name,
digitalTwinConnection.description,
digitalTwinConnection.dbType,
digitalTwinConnection.host,
digitalTwinConnection.port,
digitalTwinConnection.databaseName,
digitalTwinConnection.username,
encryptedPassword,
digitalTwinConnection.sslEnabled,
digitalTwinConnection.isActive,
]
);
connectionId = result.rows[0].id;
console.log(`✅ 새 연결 추가 완료 (ID: ${connectionId})`);
}
console.log("\n✅ 모든 테스트 완료!");
console.log(`\n📌 연결 ID: ${connectionId}`);
console.log(" 이 ID를 사용하여 플로우 관리나 제어 관리에서 외부 DB를 연동할 수 있습니다.");
} catch (error: any) {
console.error("\n❌ 오류 발생:", error.message);
console.error("상세 정보:", error);
throw error;
} finally {
await internalPool.end();
}
}
// 스크립트 실행
testDigitalTwinDb()
.then(() => {
console.log("\n🎉 스크립트 완료");
process.exit(0);
})
.catch((error) => {
console.error("\n💥 스크립트 실패:", error);
process.exit(1);
});

View File

@ -8,6 +8,7 @@ import path from "path";
import config from "./config/environment"; import config from "./config/environment";
import { logger } from "./utils/logger"; import { logger } from "./utils/logger";
import { errorHandler } from "./middleware/errorHandler"; import { errorHandler } from "./middleware/errorHandler";
import { refreshTokenIfNeeded } from "./middleware/authMiddleware";
// 라우터 임포트 // 라우터 임포트
import authRoutes from "./routes/authRoutes"; import authRoutes from "./routes/authRoutes";
@ -57,15 +58,31 @@ import riskAlertRoutes from "./routes/riskAlertRoutes"; // 리스크/알림 관
import todoRoutes from "./routes/todoRoutes"; // To-Do 관리 import todoRoutes from "./routes/todoRoutes"; // To-Do 관리
import bookingRoutes from "./routes/bookingRoutes"; // 예약 요청 관리 import bookingRoutes from "./routes/bookingRoutes"; // 예약 요청 관리
import mapDataRoutes from "./routes/mapDataRoutes"; // 지도 데이터 관리 import mapDataRoutes from "./routes/mapDataRoutes"; // 지도 데이터 관리
import yardLayoutRoutes from "./routes/yardLayoutRoutes"; // 야드 관리 3D import excelMappingRoutes from "./routes/excelMappingRoutes"; // 엑셀 매핑 템플릿
import yardLayoutRoutes from "./routes/yardLayoutRoutes"; // 3D 필드
//import materialRoutes from "./routes/materialRoutes"; // 자재 관리 //import materialRoutes from "./routes/materialRoutes"; // 자재 관리
import digitalTwinRoutes from "./routes/digitalTwinRoutes"; // 디지털 트윈 (야드 관제)
import flowRoutes from "./routes/flowRoutes"; // 플로우 관리 import flowRoutes from "./routes/flowRoutes"; // 플로우 관리
import flowExternalDbConnectionRoutes from "./routes/flowExternalDbConnectionRoutes"; // 플로우 전용 외부 DB 연결 import flowExternalDbConnectionRoutes from "./routes/flowExternalDbConnectionRoutes"; // 플로우 전용 외부 DB 연결
import workHistoryRoutes from "./routes/workHistoryRoutes"; // 작업 이력 관리 import workHistoryRoutes from "./routes/workHistoryRoutes"; // 작업 이력 관리
import tableHistoryRoutes from "./routes/tableHistoryRoutes"; // 테이블 변경 이력 조회 import tableHistoryRoutes from "./routes/tableHistoryRoutes"; // 테이블 변경 이력 조회
import roleRoutes from "./routes/roleRoutes"; // 권한 그룹 관리 import roleRoutes from "./routes/roleRoutes"; // 권한 그룹 관리
import departmentRoutes from "./routes/departmentRoutes"; // 부서 관리 import departmentRoutes from "./routes/departmentRoutes"; // 부서 관리
import tableCategoryValueRoutes from "./routes/tableCategoryValueRoutes"; // 카테고리 값 관리
import codeMergeRoutes from "./routes/codeMergeRoutes"; // 코드 병합 import codeMergeRoutes from "./routes/codeMergeRoutes"; // 코드 병합
import numberingRuleRoutes from "./routes/numberingRuleRoutes"; // 채번 규칙 관리
import entitySearchRoutes from "./routes/entitySearchRoutes"; // 엔티티 검색
import screenEmbeddingRoutes from "./routes/screenEmbeddingRoutes"; // 화면 임베딩 및 데이터 전달
import screenGroupRoutes from "./routes/screenGroupRoutes"; // 화면 그룹 관리
import vehicleTripRoutes from "./routes/vehicleTripRoutes"; // 차량 운행 이력 관리
import driverRoutes from "./routes/driverRoutes"; // 공차중계 운전자 관리
import taxInvoiceRoutes from "./routes/taxInvoiceRoutes"; // 세금계산서 관리
import cascadingRelationRoutes from "./routes/cascadingRelationRoutes"; // 연쇄 드롭다운 관계 관리
import cascadingAutoFillRoutes from "./routes/cascadingAutoFillRoutes"; // 자동 입력 관리
import cascadingConditionRoutes from "./routes/cascadingConditionRoutes"; // 조건부 연쇄 관리
import cascadingMutualExclusionRoutes from "./routes/cascadingMutualExclusionRoutes"; // 상호 배제 관리
import cascadingHierarchyRoutes from "./routes/cascadingHierarchyRoutes"; // 다단계 계층 관리
import categoryValueCascadingRoutes from "./routes/categoryValueCascadingRoutes"; // 카테고리 값 연쇄관계
import { BatchSchedulerService } from "./services/batchSchedulerService"; import { BatchSchedulerService } from "./services/batchSchedulerService";
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석 // import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
// import batchRoutes from "./routes/batchRoutes"; // 임시 주석 // import batchRoutes from "./routes/batchRoutes"; // 임시 주석
@ -160,6 +177,10 @@ const limiter = rateLimit({
}); });
app.use("/api/", limiter); app.use("/api/", limiter);
// 토큰 자동 갱신 미들웨어 (모든 API 요청에 적용)
// 토큰이 1시간 이내에 만료되는 경우 자동으로 갱신하여 응답 헤더에 포함
app.use("/api/", refreshTokenIfNeeded);
// 헬스 체크 엔드포인트 // 헬스 체크 엔드포인트
app.get("/health", (req, res) => { app.get("/health", (req, res) => {
res.status(200).json({ res.status(200).json({
@ -177,6 +198,7 @@ app.use("/api/multilang", multilangRoutes);
app.use("/api/table-management", tableManagementRoutes); app.use("/api/table-management", tableManagementRoutes);
app.use("/api/table-management", entityJoinRoutes); // 🎯 Entity 조인 기능 app.use("/api/table-management", entityJoinRoutes); // 🎯 Entity 조인 기능
app.use("/api/screen-management", screenManagementRoutes); app.use("/api/screen-management", screenManagementRoutes);
app.use("/api/screen-groups", screenGroupRoutes); // 화면 그룹 관리
app.use("/api/common-codes", commonCodeRoutes); app.use("/api/common-codes", commonCodeRoutes);
app.use("/api/dynamic-form", dynamicFormRoutes); app.use("/api/dynamic-form", dynamicFormRoutes);
app.use("/api/files", fileRoutes); app.use("/api/files", fileRoutes);
@ -201,6 +223,7 @@ app.use("/api/external-rest-api-connections", externalRestApiConnectionRoutes);
app.use("/api/multi-connection", multiConnectionRoutes); app.use("/api/multi-connection", multiConnectionRoutes);
app.use("/api/screen-files", screenFileRoutes); app.use("/api/screen-files", screenFileRoutes);
app.use("/api/batch-configs", batchRoutes); app.use("/api/batch-configs", batchRoutes);
app.use("/api/excel-mapping", excelMappingRoutes); // 엑셀 매핑 템플릿
app.use("/api/batch-management", batchManagementRoutes); app.use("/api/batch-management", batchManagementRoutes);
app.use("/api/batch-execution-logs", batchExecutionLogRoutes); app.use("/api/batch-execution-logs", batchExecutionLogRoutes);
// app.use("/api/db-type-categories", dbTypeCategoryRoutes); // 파일이 존재하지 않음 // app.use("/api/db-type-categories", dbTypeCategoryRoutes); // 파일이 존재하지 않음
@ -217,15 +240,29 @@ app.use("/api/risk-alerts", riskAlertRoutes); // 리스크/알림 관리
app.use("/api/todos", todoRoutes); // To-Do 관리 app.use("/api/todos", todoRoutes); // To-Do 관리
app.use("/api/bookings", bookingRoutes); // 예약 요청 관리 app.use("/api/bookings", bookingRoutes); // 예약 요청 관리
app.use("/api/map-data", mapDataRoutes); // 지도 데이터 조회 app.use("/api/map-data", mapDataRoutes); // 지도 데이터 조회
app.use("/api/yard-layouts", yardLayoutRoutes); // 야드 관리 3D app.use("/api/yard-layouts", yardLayoutRoutes); // 3D 필드
// app.use("/api/materials", materialRoutes); // 자재 관리 (임시 주석) // app.use("/api/materials", materialRoutes); // 자재 관리 (임시 주석)
app.use("/api/digital-twin", digitalTwinRoutes); // 디지털 트윈 (야드 관제)
app.use("/api/flow-external-db", flowExternalDbConnectionRoutes); // 플로우 전용 외부 DB 연결 app.use("/api/flow-external-db", flowExternalDbConnectionRoutes); // 플로우 전용 외부 DB 연결
app.use("/api/flow", flowRoutes); // 플로우 관리 (마지막에 등록하여 다른 라우트와 충돌 방지) app.use("/api/flow", flowRoutes); // 플로우 관리 (마지막에 등록하여 다른 라우트와 충돌 방지)
app.use("/api/work-history", workHistoryRoutes); // 작업 이력 관리 app.use("/api/work-history", workHistoryRoutes); // 작업 이력 관리
app.use("/api/table-history", tableHistoryRoutes); // 테이블 변경 이력 조회 app.use("/api/table-history", tableHistoryRoutes); // 테이블 변경 이력 조회
app.use("/api/roles", roleRoutes); // 권한 그룹 관리 app.use("/api/roles", roleRoutes); // 권한 그룹 관리
app.use("/api/departments", departmentRoutes); // 부서 관리 app.use("/api/departments", departmentRoutes); // 부서 관리
app.use("/api/table-categories", tableCategoryValueRoutes); // 카테고리 값 관리
app.use("/api/code-merge", codeMergeRoutes); // 코드 병합 app.use("/api/code-merge", codeMergeRoutes); // 코드 병합
app.use("/api/numbering-rules", numberingRuleRoutes); // 채번 규칙 관리
app.use("/api/entity-search", entitySearchRoutes); // 엔티티 검색
app.use("/api/driver", driverRoutes); // 공차중계 운전자 관리
app.use("/api/tax-invoice", taxInvoiceRoutes); // 세금계산서 관리
app.use("/api/cascading-relations", cascadingRelationRoutes); // 연쇄 드롭다운 관계 관리
app.use("/api/cascading-auto-fill", cascadingAutoFillRoutes); // 자동 입력 관리
app.use("/api/cascading-conditions", cascadingConditionRoutes); // 조건부 연쇄 관리
app.use("/api/cascading-exclusions", cascadingMutualExclusionRoutes); // 상호 배제 관리
app.use("/api/cascading-hierarchy", cascadingHierarchyRoutes); // 다단계 계층 관리
app.use("/api/category-value-cascading", categoryValueCascadingRoutes); // 카테고리 값 연쇄관계
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
app.use("/api/vehicle", vehicleTripRoutes); // 차량 운행 이력 관리
// app.use("/api/collections", collectionRoutes); // 임시 주석 // app.use("/api/collections", collectionRoutes); // 임시 주석
// app.use("/api/batch", batchRoutes); // 임시 주석 // app.use("/api/batch", batchRoutes); // 임시 주석
// app.use('/api/users', userRoutes); // app.use('/api/users', userRoutes);
@ -270,7 +307,7 @@ app.listen(PORT, HOST, async () => {
// 배치 스케줄러 초기화 // 배치 스케줄러 초기화
try { try {
await BatchSchedulerService.initialize(); await BatchSchedulerService.initializeScheduler();
logger.info(`⏰ 배치 스케줄러가 시작되었습니다.`); logger.info(`⏰ 배치 스케줄러가 시작되었습니다.`);
} catch (error) { } catch (error) {
logger.error(`❌ 배치 스케줄러 초기화 실패:`, error); logger.error(`❌ 배치 스케줄러 초기화 실패:`, error);

View File

@ -1,4 +1,7 @@
import { Response } from "express"; import { Response } from "express";
import https from "https";
import axios, { AxiosRequestConfig } from "axios";
import { logger } from "../utils/logger";
import { AuthenticatedRequest } from "../middleware/authMiddleware"; import { AuthenticatedRequest } from "../middleware/authMiddleware";
import { DashboardService } from "../services/DashboardService"; import { DashboardService } from "../services/DashboardService";
import { import {
@ -7,6 +10,7 @@ import {
DashboardListQuery, DashboardListQuery,
} from "../types/dashboard"; } from "../types/dashboard";
import { PostgreSQLService } from "../database/PostgreSQLService"; import { PostgreSQLService } from "../database/PostgreSQLService";
import { ExternalRestApiConnectionService } from "../services/externalRestApiConnectionService";
/** /**
* *
@ -41,6 +45,7 @@ export class DashboardController {
isPublic = false, isPublic = false,
tags, tags,
category, category,
settings,
}: CreateDashboardRequest = req.body; }: CreateDashboardRequest = req.body;
// 유효성 검증 // 유효성 검증
@ -85,6 +90,7 @@ export class DashboardController {
elements, elements,
tags, tags,
category, category,
settings,
}; };
// console.log('대시보드 생성 시작:', { title: dashboardData.title, userId, elementsCount: elements.length }); // console.log('대시보드 생성 시작:', { title: dashboardData.title, userId, elementsCount: elements.length });
@ -413,7 +419,7 @@ export class DashboardController {
limit: Math.min(parseInt(req.query.limit as string) || 20, 100), limit: Math.min(parseInt(req.query.limit as string) || 20, 100),
search: req.query.search as string, search: req.query.search as string,
category: req.query.category as string, category: req.query.category as string,
createdBy: userId, // 본인이 만든 대시보드만 // createdBy 제거 - 회사 대시보드 전체 표시
}; };
const result = await DashboardService.getDashboards( const result = await DashboardService.getDashboards(
@ -588,7 +594,14 @@ export class DashboardController {
res: Response res: Response
): Promise<void> { ): Promise<void> {
try { try {
const { url, method = "GET", headers = {}, queryParams = {} } = req.body; const {
url,
method = "GET",
headers = {},
queryParams = {},
body,
externalConnectionId, // 프론트엔드에서 선택된 커넥션 ID를 전달받아야 함
} = req.body;
if (!url || typeof url !== "string") { if (!url || typeof url !== "string") {
res.status(400).json({ res.status(400).json({
@ -606,85 +619,175 @@ export class DashboardController {
} }
}); });
// 외부 API 호출 (타임아웃 30초) // Axios 요청 설정
// @ts-ignore - node-fetch dynamic import const requestConfig: AxiosRequestConfig = {
const fetch = (await import("node-fetch")).default; url: urlObj.toString(),
method: method.toUpperCase(),
headers: {
"Content-Type": "application/json",
Accept: "application/json",
...headers,
},
timeout: 60000, // 60초 타임아웃
validateStatus: () => true, // 모든 상태 코드 허용 (에러도 응답으로 처리)
};
// 타임아웃 설정 (Node.js 글로벌 AbortController 사용) // 연결 정보 (응답에 포함용)
const controller = new (global as any).AbortController(); let connectionInfo: { saveToHistory?: boolean } | null = null;
const timeoutId = setTimeout(() => controller.abort(), 60000); // 60초 (기상청 API는 느림)
let response; // 외부 커넥션 ID가 있는 경우, 해당 커넥션의 인증 정보(DB 토큰 등)를 적용
try { if (externalConnectionId) {
response = await fetch(urlObj.toString(), { try {
method: method.toUpperCase(), // 사용자 회사 코드가 있으면 사용하고, 없으면 '*' (최고 관리자)로 시도
headers: { let companyCode = req.user?.companyCode;
"Content-Type": "application/json",
...headers, if (!companyCode) {
}, companyCode = "*";
signal: controller.signal, }
});
clearTimeout(timeoutId); // 커넥션 로드
} catch (err: any) { const connectionResult =
clearTimeout(timeoutId); await ExternalRestApiConnectionService.getConnectionById(
if (err.name === 'AbortError') { Number(externalConnectionId),
throw new Error('외부 API 요청 타임아웃 (30초 초과)'); companyCode
);
if (connectionResult.success && connectionResult.data) {
const connection = connectionResult.data;
// 연결 정보 저장 (응답에 포함)
connectionInfo = {
saveToHistory: connection.save_to_history === "Y",
};
// 인증 헤더 생성 (DB 토큰 등)
const authHeaders =
await ExternalRestApiConnectionService.getAuthHeaders(
connection.auth_type,
connection.auth_config,
connection.company_code
);
// 기존 헤더에 인증 헤더 병합
requestConfig.headers = {
...requestConfig.headers,
...authHeaders,
};
// API Key가 Query Param인 경우 처리
if (
connection.auth_type === "api-key" &&
connection.auth_config?.keyLocation === "query" &&
connection.auth_config?.keyName &&
connection.auth_config?.keyValue
) {
const currentUrl = new URL(requestConfig.url!);
currentUrl.searchParams.append(
connection.auth_config.keyName,
connection.auth_config.keyValue
);
requestConfig.url = currentUrl.toString();
}
}
} catch (connError) {
logger.error(
`외부 커넥션(${externalConnectionId}) 정보 로드 및 인증 적용 실패:`,
connError
);
} }
throw err;
} }
if (!response.ok) { // Body 처리
if (body) {
requestConfig.data = body;
}
// 디버깅 로그: 실제 요청 정보 출력
logger.info(`[fetchExternalApi] 요청 정보:`, {
url: requestConfig.url,
method: requestConfig.method,
headers: requestConfig.headers,
body: requestConfig.data,
externalConnectionId,
});
// TLS 인증서 검증 예외 처리 (thiratis.com 등 내부망/레거시 API 대응)
// ExternalRestApiConnectionService와 동일한 로직 적용
const bypassDomains = ["thiratis.com"];
const hostname = urlObj.hostname;
const shouldBypassTls = bypassDomains.some((domain) =>
hostname.includes(domain)
);
if (shouldBypassTls) {
requestConfig.httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
}
// 기상청 API 등 EUC-KR 인코딩을 사용하는 경우 arraybuffer로 받아서 디코딩
const isKmaApi = urlObj.hostname.includes("kma.go.kr");
if (isKmaApi) {
requestConfig.responseType = "arraybuffer";
}
const response = await axios(requestConfig);
if (response.status >= 400) {
throw new Error( throw new Error(
`외부 API 오류: ${response.status} ${response.statusText}` `외부 API 오류: ${response.status} ${response.statusText}`
); );
} }
// Content-Type에 따라 응답 파싱 let data = response.data;
const contentType = response.headers.get("content-type"); const contentType = response.headers["content-type"];
let data: any;
// 한글 인코딩 처리 (EUC-KR → UTF-8) // 기상청 API 인코딩 처리 (UTF-8 우선, 실패 시 EUC-KR)
const isKoreanApi = urlObj.hostname.includes('kma.go.kr') || if (isKmaApi && Buffer.isBuffer(data)) {
urlObj.hostname.includes('data.go.kr'); const iconv = require("iconv-lite");
const buffer = Buffer.from(data);
const utf8Text = buffer.toString("utf-8");
if (isKoreanApi) { // UTF-8로 정상 디코딩되었는지 확인
// 한국 정부 API는 EUC-KR 인코딩 사용 if (
const buffer = await response.arrayBuffer(); utf8Text.includes("특보") ||
const decoder = new TextDecoder('euc-kr'); utf8Text.includes("경보") ||
const text = decoder.decode(buffer); utf8Text.includes("주의보") ||
(utf8Text.includes("#START7777") && !utf8Text.includes("<22>"))
try { ) {
data = JSON.parse(text); data = { text: utf8Text, contentType, encoding: "utf-8" };
} catch { } else {
data = { text, contentType }; // EUC-KR로 디코딩
} const eucKrText = iconv.decode(buffer, "EUC-KR");
} else if (contentType && contentType.includes("application/json")) { data = { text: eucKrText, contentType, encoding: "euc-kr" };
data = await response.json();
} else if (contentType && contentType.includes("text/")) {
// 텍스트 응답 (CSV, 일반 텍스트 등)
const text = await response.text();
data = { text, contentType };
} else {
// 기타 응답 (JSON으로 시도)
try {
data = await response.json();
} catch {
const text = await response.text();
data = { text, contentType };
} }
} }
// 텍스트 응답인 경우 포맷팅
else if (typeof data === "string") {
data = { text: data, contentType };
}
res.status(200).json({ res.status(200).json({
success: true, success: true,
data, data,
connectionInfo, // 외부 연결 정보 (saveToHistory 등)
}); });
} catch (error) { } catch (error: any) {
const status = error.response?.status || 500;
const message = error.response?.statusText || error.message;
logger.error("외부 API 호출 오류:", {
message,
status,
data: error.response?.data,
});
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "외부 API 호출 중 오류가 발생했습니다.", message: "외부 API 호출 중 오류가 발생했습니다.",
error: error:
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? (error as Error).message ? message
: "외부 API 호출 오류", : "외부 API 호출 오류",
}); });
} }

File diff suppressed because it is too large Load Diff

View File

@ -141,6 +141,110 @@ export class AuthController {
} }
} }
/**
* POST /api/auth/switch-company
* WACE 전용: 다른
*/
static async switchCompany(req: Request, res: Response): Promise<void> {
try {
const { companyCode } = req.body;
const authHeader = req.get("Authorization");
const token = authHeader && authHeader.split(" ")[1];
if (!token) {
res.status(401).json({
success: false,
message: "인증 토큰이 필요합니다.",
error: { code: "TOKEN_MISSING" },
});
return;
}
// 현재 사용자 정보 확인
const currentUser = JwtUtils.verifyToken(token);
// WACE 관리자 권한 체크 (userType = "SUPER_ADMIN"만 확인)
// 이미 다른 회사로 전환한 상태(companyCode != "*")에서도 다시 전환 가능해야 함
if (currentUser.userType !== "SUPER_ADMIN") {
logger.warn(`회사 전환 권한 없음: userId=${currentUser.userId}, userType=${currentUser.userType}, companyCode=${currentUser.companyCode}`);
res.status(403).json({
success: false,
message: "회사 전환은 최고 관리자(SUPER_ADMIN)만 가능합니다.",
error: { code: "FORBIDDEN" },
});
return;
}
// 전환할 회사 코드 검증
if (!companyCode || companyCode.trim() === "") {
res.status(400).json({
success: false,
message: "전환할 회사 코드가 필요합니다.",
error: { code: "INVALID_INPUT" },
});
return;
}
logger.info(`=== WACE 관리자 회사 전환 ===`, {
userId: currentUser.userId,
originalCompanyCode: currentUser.companyCode,
targetCompanyCode: companyCode,
});
// 회사 코드 존재 여부 확인 (company_code가 "*"가 아닌 경우만)
if (companyCode !== "*") {
const { query } = await import("../database/db");
const companies = await query<any>(
"SELECT company_code, company_name FROM company_mng WHERE company_code = $1",
[companyCode]
);
if (companies.length === 0) {
res.status(404).json({
success: false,
message: "존재하지 않는 회사 코드입니다.",
error: { code: "COMPANY_NOT_FOUND" },
});
return;
}
}
// 새로운 JWT 토큰 발급 (company_code만 변경)
const newPersonBean: PersonBean = {
...currentUser,
companyCode: companyCode.trim(), // 전환할 회사 코드로 변경
};
const newToken = JwtUtils.generateToken(newPersonBean);
logger.info(`✅ 회사 전환 성공: ${currentUser.userId}${companyCode}`);
res.status(200).json({
success: true,
message: "회사 전환 완료",
data: {
token: newToken,
companyCode: companyCode.trim(),
},
});
} catch (error) {
logger.error(
`회사 전환 API 오류: ${error instanceof Error ? error.message : error}`
);
res.status(500).json({
success: false,
message: "회사 전환 중 오류가 발생했습니다.",
error: {
code: "SERVER_ERROR",
details:
error instanceof Error
? error.message
: "알 수 없는 오류가 발생했습니다.",
},
});
}
}
/** /**
* POST /api/auth/logout * POST /api/auth/logout
* Java ApiLoginController.logout() * Java ApiLoginController.logout()
@ -226,13 +330,14 @@ export class AuthController {
} }
// 프론트엔드 호환성을 위해 더 많은 사용자 정보 반환 // 프론트엔드 호환성을 위해 더 많은 사용자 정보 반환
// ⚠️ JWT 토큰의 companyCode를 우선 사용 (회사 전환 기능 지원)
const userInfoResponse: any = { const userInfoResponse: any = {
userId: dbUserInfo.userId, userId: dbUserInfo.userId,
userName: dbUserInfo.userName || "", userName: dbUserInfo.userName || "",
deptName: dbUserInfo.deptName || "", deptName: dbUserInfo.deptName || "",
companyCode: dbUserInfo.companyCode || "ILSHIN", companyCode: userInfo.companyCode || dbUserInfo.companyCode || "ILSHIN", // JWT 토큰 우선
company_code: dbUserInfo.companyCode || "ILSHIN", // 프론트엔드 호환성 company_code: userInfo.companyCode || dbUserInfo.companyCode || "ILSHIN", // JWT 토큰 우선
userType: dbUserInfo.userType || "USER", userType: userInfo.userType || dbUserInfo.userType || "USER", // JWT 토큰 우선
userTypeName: dbUserInfo.userTypeName || "일반사용자", userTypeName: dbUserInfo.userTypeName || "일반사용자",
email: dbUserInfo.email || "", email: dbUserInfo.email || "",
photo: dbUserInfo.photo, photo: dbUserInfo.photo,
@ -384,4 +489,69 @@ export class AuthController {
}); });
} }
} }
/**
* POST /api/auth/signup
* API
*/
static async signup(req: Request, res: Response): Promise<void> {
try {
const { userId, password, userName, phoneNumber, licenseNumber, vehicleNumber, vehicleType } = req.body;
logger.info(`=== 공차중계 회원가입 API 호출 ===`);
logger.info(`userId: ${userId}, vehicleNumber: ${vehicleNumber}`);
// 입력값 검증
if (!userId || !password || !userName || !phoneNumber || !licenseNumber || !vehicleNumber) {
res.status(400).json({
success: false,
message: "필수 입력값이 누락되었습니다.",
error: {
code: "INVALID_INPUT",
details: "아이디, 비밀번호, 이름, 연락처, 면허번호, 차량번호는 필수입니다.",
},
});
return;
}
// 회원가입 처리
const signupResult = await AuthService.signupDriver({
userId,
password,
userName,
phoneNumber,
licenseNumber,
vehicleNumber,
vehicleType,
});
if (signupResult.success) {
logger.info(`공차중계 회원가입 성공: ${userId}`);
res.status(201).json({
success: true,
message: "회원가입이 완료되었습니다.",
});
} else {
logger.warn(`공차중계 회원가입 실패: ${userId} - ${signupResult.message}`);
res.status(400).json({
success: false,
message: signupResult.message || "회원가입에 실패했습니다.",
error: {
code: "SIGNUP_FAILED",
details: signupResult.message,
},
});
}
} catch (error) {
logger.error("공차중계 회원가입 API 오류:", error);
res.status(500).json({
success: false,
message: "회원가입 처리 중 오류가 발생했습니다.",
error: {
code: "SIGNUP_ERROR",
details: error instanceof Error ? error.message : "알 수 없는 오류",
},
});
}
}
} }

Some files were not shown because too many files have changed in this diff Show More