Compare commits

...

201 Commits

Author SHA1 Message Date
kjs 1ee946d712 Merge pull request 'jskim-node' (#405) from jskim-node into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/405
2026-03-05 21:46:50 +09:00
kjs db31b02180 Merge branch 'barcode' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-03-05 21:46:36 +09:00
kjs ee1760da2f Merge branch 'main' into barcode 2026-03-05 21:46:12 +09:00
chpark 7ab05aea72 바코드 업그레이드드 2026-03-05 21:45:26 +09:00
kjs ea6c5ac43c fix: Remove unnecessary whitespace in SplitPanelLayoutComponent
- Cleaned up the code by removing an unnecessary blank line in the SplitPanelLayoutComponent.tsx file.
- This minor adjustment improves code readability without affecting functionality.
2026-03-05 21:41:53 +09:00
kjs ea0123d6cc Merge pull request 'jskim-node' (#404) from jskim-node into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/404
2026-03-05 19:30:38 +09:00
kjs 93eaf59966 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-03-05 19:27:51 +09:00
kjs c56f434ff1 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-03-05 19:27:33 +09:00
kjs 536982dd71 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node 2026-03-05 19:21:50 +09:00
kjs 0e8c68a9ff feat: Add multi-table Excel upload functionality
- Implemented new API endpoints for multi-table Excel upload and auto-detection of table chains.
- The GET endpoint `/api/data/multi-table/auto-detect` allows automatic detection of foreign key relationships based on the provided root table.
- The POST endpoint `/api/data/multi-table/upload` handles the upload of multi-table data, including validation and logging of the upload process.
- Updated the frontend to include options for multi-table Excel upload in the button configuration panel and integrated the corresponding action handler.

This feature enhances the data management capabilities by allowing users to upload and manage data across multiple related tables efficiently.
2026-03-05 19:17:35 +09:00
chpark bfd97c9717 바코드 업데이트 및 AI LLm 적용용 2026-03-05 19:08:08 +09:00
SeongHyun Kim a81cb7ca19 Merge branch 'ksh-v2-work' 2026-03-05 19:04:54 +09:00
SeongHyun Kim 12a8290873 feat(pop): 설정 패널 아코디언 접기/펼치기 일관성 + sessionStorage 상태 기억
설정 패널을 열 때 섹션이 일부는 펼쳐져 있고 일부는 접혀 있어
일관성이 없던 UX를 개선하고, 사용자가 펼친 섹션을 탭 세션 내에서 기억한다.
- useCollapsibleSections 커스텀 훅 생성 (sessionStorage 기반, 초기 모두 접힘)
- PopCardListConfig: CollapsibleSection에 sectionKey/sections prop 패턴 적용
- PopFieldConfig: SaveTabContent 5개 고정 섹션 훅 적용,
  SectionEditor 초기값 접힘으로 변경
- PopDashboardConfig: PageEditor 초기값 접힘으로 변경
2026-03-05 18:54:29 +09:00
SeongHyun Kim 7a9a705f19 feat(pop-card-list): 포장 요약 바 UI + 카드 레이아웃 flex column 개선
포장 입력 완료 시 카드 하단에 포장 내역 요약을 표시하여
디자이너가 포장 계산 결과를 즉시 확인할 수 있도록 한다.
- 카드 하단에 포장 요약 바 추가 (emerald 테마, 포장완료 뱃지)
- height(고정) -> minHeight(유동)으로 카드 자연 성장 허용
- gridAutoRows를 minmax(높이, auto)로 변경 (그리드 셀도 성장)
- 카드 flex flex-col + 본문 flex-1 overflow-hidden 구조
- 오른쪽 버튼 영역 justify-center -> justify-start (위쪽 정렬)
2026-03-05 18:34:45 +09:00
SeongHyun Kim 85bf4882a8 fix(pop-card-list): 미입고 formula 필드 입력값 연동 복원
설정 UI에서 formulaRightType 기본값을 "input"으로 표시하지만
DB에 명시적으로 저장하지 않아, 렌더링 시 undefined === "input"이
false가 되어 입력필드 연동이 작동하지 않던 버그를 수정한다.
- FieldRow: (field.formulaRightType || "input") === "input"으로
  기본값 fallback 추가
2026-03-05 18:00:17 +09:00
SeongHyun Kim b2b0b575df feat(pop): 버튼 v2 통합 아키텍처 + data-update 연산 확장 (BLOCK M + N)
버튼 컴포넌트의 실행 경로를 프리셋별 파편화에서 단일 작업 목록(task-list)
패턴으로 통합하고, 부분입고 시나리오 지원을 위해 data-update 연산을 확장한다.
[BLOCK M: 버튼 v2 통합 아키텍처]
- ButtonTask 타입 체계 정의 (10종 작업 타입 + UpdateOperation)
- PopButtonConfigV2 + migrateButtonConfig 자동 마이그레이션
- 설정 UI: 빠른 시작 + 외형 + 작업 목록 에디터
- executeTaskList 범용 실행 함수 (데이터 작업 일괄 백엔드 전달)
- collect_data 프로토콜에 cartChanges 포함
- 백엔드 tasks 배열 기반 처리 (data-save/update/delete/cart-save)
- useCartSync.getChanges() 추출 + 카드리스트 응답 포함
[BLOCK N: data-update 연산 확장]
- UpdateOperationType에 multiply, divide, db-conditional 추가
- ButtonTask에 db-conditional 전용 필드 5개 추가
  (compareColumn, compareOperator, compareWith, dbThenValue, dbElseValue)
- 설정 UI: 드롭다운 3개 옵션 + DB 컬럼 비교 설정 폼
- 백엔드 SQL: multiply, divide(0-division 방어),
  db-conditional(CASE WHEN 배치 UPDATE)
- 기존 add/subtract에 ::numeric 캐스팅 일관 적용
2026-03-05 17:22:30 +09:00
kjs f7bd2f6fa3 Merge pull request 'jskim-node' (#402) from jskim-node into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/402
2026-03-05 13:32:16 +09:00
kjs 7e2ae4335e Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-03-05 13:31:39 +09:00
kjs d58131d88d Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-03-05 13:31:33 +09:00
kjs 1917b7253d Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-03-05 13:31:19 +09:00
kjs 9f9b130738 Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-03-05 13:31:13 +09:00
SeongHyun Kim 91c9dda6ae feat(pop-field): 숨은 필드 고정값 + Select 데이터 연동(linkedFilters) 구현
입고 확정 시 status/inbound_status가 빈 값으로 저장되는 문제(FIX-3)와
창고내 위치 셀렉트가 전체 위치를 보여주는 문제를 해결한다.
[FIX-3: 숨은 필드 고정값]
- types.ts: HiddenValueSource에 "static" 추가, staticValue 필드
- PopFieldConfig: 숨은 필드 설정 UI에 "고정값" 모드 추가
- PopFieldComponent: collected_data에 hiddenMappings 포함
- popActionRoutes: INSERT 시 hiddenMappings 값 주입
[Select 데이터 연동 - BLOCK L]
- types.ts: SelectLinkedFilter 인터페이스 + FieldSelectSource.linkedFilters
- PopFieldConfig: "데이터 연동" 토글 + LinkedFiltersEditor 컴포넌트
  (섹션 내 필드 선택 → 필터 컬럼 매핑)
- PopFieldComponent: fieldIdToName 맵으로 id-name 변환,
  SelectFieldInput에서 연동 필드 값 변경 시 동적 필터 재조회,
  상위 미선택 시 안내 메시지, 상위 변경 시 하위 자동 초기화
2026-03-05 12:13:07 +09:00
DDD1542 d43f0821ed refactor: Update authentication handling in authRoutes and useAuth hook
- Replaced the middleware `checkAuthStatus` with the `AuthController.checkAuthStatus` method in the authentication routes for improved clarity and structure.
- Simplified token validation logic in the `useAuth` hook by removing unnecessary checks for expired tokens, allowing the API client to handle token refresh automatically.
- Enhanced logging for authentication checks to provide clearer insights into the authentication flow and potential issues.
- Adjusted the handling of user authentication status to ensure consistency and reliability in user state management.

This refactor streamlines the authentication process and improves the overall maintainability of the authentication logic.
2026-03-05 11:51:05 +09:00
DDD1542 4b8f2b7839 feat: Update screen reference handling in V2 layouts
- Enhanced the `ScreenManagementService` to include updates for V2 layouts in the `screen_layouts_v2` table.
- Implemented logic to remap `screenId`, `targetScreenId`, `modalScreenId`, and other related IDs in layout data.
- Added logging for the number of layouts updated in both V1 and V2, improving traceability of the update process.
- This update ensures that screen references are correctly maintained across different layout versions, enhancing the overall functionality of the screen management system.
2026-03-05 11:30:31 +09:00
DDD1542 4f639dec34 feat: Implement screen group screens duplication in menu copy service
- Added a new method `copyScreenGroupScreens` to handle the duplication of screen group screens during the menu copy process.
- Implemented logic to create a mapping of screen group IDs from the source to the target company.
- Enhanced the existing menu copy functionality to include the copying of screen group screens, ensuring that the screen-role and display order are preserved.
- Added logging for better traceability of the duplication process.

This update improves the menu copy service by allowing for a more comprehensive duplication of associated screen group screens, enhancing the overall functionality of the menu management system.
2026-03-05 10:09:37 +09:00
DDD1542 772514c270 Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into gbpark-node 2026-03-05 09:59:23 +09:00
DDD1542 6f7e2b1b0c Merge remote-tracking branch 'origin/feature/v2-renewal' 2026-03-05 01:31:02 +09:00
DDD1542 b9080d03f6 Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into gbpark-node 2026-03-05 01:30:07 +09:00
DDD1542 8d0f2dbb27 fix: 멀티 레이어 화면 복제 버그 수정
- menuCopyService: 모든 레이어 순회 처리, ON CONFLICT 제약조건 수정
- screenManagementService: layer_id 하드코딩 제거, 멀티 레이어 순회
- screen_conditional_zones 복제 로직 추가 (zoneIdMap 생성)
- condition_config의 zone_id 재매핑 로직 추가
- 레이어 수 변경 감지를 위한 변경 비교 로직 보강

Made-with: Cursor
2026-03-05 01:29:56 +09:00
kjs a69b135b65 Merge pull request 'feature/v2-renewal' (#401) from feature/v2-renewal into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/401
2026-03-04 23:43:25 +09:00
kjs fcb122c58b Merge branch 'main' into feature/v2-renewal 2026-03-04 23:43:18 +09:00
DDD1542 e11a7b1237 Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into gbpark-node 2026-03-04 23:42:45 +09:00
DDD1542 366cfcde60 feat: Enhance menu copy and screen sync services with new fields
- Added `menu_icon` and `screen_group_id` fields to the Menu interface in `menuCopyService.ts` to support additional menu attributes.
- Updated the SQL insert statements in both `menuCopyService.ts` and `menuScreenSyncService.ts` to include the new fields, ensuring that menu icons and screen group IDs are properly handled during menu creation and synchronization.
- These enhancements improve the flexibility and functionality of the menu management system.
2026-03-04 23:42:43 +09:00
kjs 5b6b4be73c Merge pull request 'feature/v2-renewal' (#400) from feature/v2-renewal into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/400
2026-03-04 23:03:02 +09:00
kjs b40f6c28dc Merge branch 'main' into feature/v2-renewal 2026-03-04 23:02:27 +09:00
DDD1542 d31568b1bd feat: Enhance menu and screen synchronization services with new fields
- Added `menu_icon` and `screen_group_id` fields to the `Menu` interface in `menuCopyService.ts` to support additional menu attributes.
- Updated the SQL insert statements in both `menuCopyService.ts` and `menuScreenSyncService.ts` to include the new fields, ensuring that menu icons and screen group IDs are properly handled during menu creation and synchronization.
- These enhancements improve the flexibility and functionality of the menu management system, allowing for richer menu configurations.
2026-03-04 22:46:02 +09:00
DDD1542 3a3e4e8926 Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into gbpark-node
; 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-03-04 22:45:40 +09:00
kjs 818cc80514 Merge pull request 'jskim-node' (#399) from jskim-node into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/399
2026-03-04 21:17:11 +09:00
kjs 58a62a6d4e Merge branch 'main' into jskim-node 2026-03-04 21:17:04 +09:00
kjs b950a102f1 Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-03-04 21:16:48 +09:00
kjs a6d6b03195 fix: Update resource naming conventions in controllers
- Refactored resource naming in the CommonCodeController to use `category_code` and `category_name` for consistency.
- Adjusted the NumberingRuleController to replace `prefix` with `separator` in the changes log for clarity.
- Modified the ScreenManagementController to utilize `screenId` instead of `id` for resource identification, ensuring uniformity across the application.
- These changes enhance the readability and maintainability of the codebase by standardizing naming conventions.
2026-03-04 21:16:46 +09:00
kjs 359e333370 Merge pull request 'jskim-node' (#398) from jskim-node into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/398
2026-03-04 21:10:43 +09:00
kjs 95c42a971c Merge branch 'main' into jskim-node 2026-03-04 21:09:55 +09:00
kjs ac2da7a1d7 feat: Implement entity join functionality in V2Repeater and configuration panel
- Added support for entity joins in the V2Repeater component, allowing for automatic resolution of foreign key references to display data from related tables.
- Introduced a new `resolveEntityJoins` function to handle the fetching and mapping of reference data based on configured entity joins.
- Enhanced the V2RepeaterConfigPanel to manage entity join configurations, including loading available columns and toggling join settings.
- Updated the data handling logic to incorporate mapping rules for incoming data, ensuring that only necessary fields are retained during processing.
- Improved user experience by providing clear logging and feedback during entity join resolution and data mapping operations.
2026-03-04 21:08:45 +09:00
kjs 4da5d10f09 Merge pull request '바코드 기능 커밋밋' (#397) from barcode into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/397
2026-03-04 20:54:52 +09:00
kjs 5f3b144b12 Merge branch 'main' into barcode 2026-03-04 20:54:46 +09:00
chpark b9c0a0f243 바코드 기능 커밋밋 2026-03-04 20:51:00 +09:00
SeongHyun Kim a6c0ab5664 feat(pop): 입고 확정 시 자동 채번 실행 + 결과 모달 UX + 셀렉트 높이 통일
입고 확정(inbound-confirm) 실행 시 채번 규칙이 설정되어 있어도
inbound_number가 null로 저장되던 문제를 해결한다.
[채번 실행 (FIX-1)]
- types.ts: SaveMapping에 autoGenMappings 필드 추가 (numberingRuleId,
  targetColumn, showResultModal)
- PopFieldComponent: collect_data 응답에 autoGenMappings 포함하여
  백엔드에 채번 규칙 정보 전달
- popActionRoutes: INSERT 전 numberingRuleService.allocateCode() 호출,
  생성된 코드를 generatedCodes 배열로 응답에 포함
[결과 모달 UX]
- pop-button: showResultModal 토글에 따라 채번 결과 모달 표시 분기
- 모달이 열려 있는 동안 followUpActions(refresh/navigate) 지연하여
  사용자가 확인 버튼을 눌러야 후속 액션 실행
[셀렉트 높이 일관성]
- SelectTrigger hasCustomHeight에 /\bh-\d/ 패턴 추가하여
  className의 h-9 등이 기본 data-size="xs"(h-6)와 충돌하지 않도록 수정
[기타 수정]
- SelectFieldInput: Set 기반 dedup으로 React key 중복 방지
- PopFieldConfig: AutoNumberEditor 제거, 채번 규칙을 저장 탭에서 관리
- PopFieldConfig: 전체 채번 규칙 보기 토글 추가
- PopCardListComponent: 장바구니 목록 모드에서 수량 자동 초기화 방지
- PopCardListConfig: 수식 필드 매핑 노출 + 누락 필드 자동 추가
2026-03-04 19:12:22 +09:00
kjs f97edad1ea feat: Enhance screen group deletion functionality with optional numbering rules deletion
- Added a new query parameter `deleteNumberingRules` to the `deleteScreenGroup` function, allowing users to specify if numbering rules should be deleted when a root screen group is removed.
- Updated the `deleteScreenGroup` controller to handle the deletion of numbering rules conditionally based on the new parameter.
- Enhanced the frontend `ScreenGroupTreeView` component to include a checkbox for users to confirm the deletion of numbering rules when deleting a root group, improving user control and clarity during deletion operations.
- Implemented appropriate warnings and messages to inform users about the implications of deleting numbering rules, ensuring better user experience and data integrity awareness.
2026-03-04 18:42:44 +09:00
DDD1542 f6a2668bdc feat: Enhance approval request handling and user management
- Updated the approval request controller to include target_record_id in query parameters for improved filtering.
- Refactored the approval request creation logic to merge approval_mode into target_record_data, allowing for better handling of approval processes.
- Enhanced the user search functionality in the approval request modal to accommodate additional user attributes such as position and department.
- Improved error handling messages for clarity regarding required fields in the approval request modal.
- Added new menu item for accessing the approval box directly from user dropdown and app layout.

Made-with: Cursor
2026-03-04 18:26:16 +09:00
kjs 93d9df3e5a feat: Refactor category mapping logic in TableListComponent
- Introduced a helper function to flatten tree structures for category mappings, improving code readability and maintainability.
- Removed redundant checks for empty category columns, streamlining the data fetching process.
- Enhanced error handling for category value loading, ensuring clearer logging of failures.
- Updated the mapping logic to utilize the new flattening function, ensuring consistent handling of category data across components.
2026-03-04 16:41:51 +09:00
kjs ea8b4ce5dc chore: Remove obsolete .classpath file
- Deleted the .classpath file as it is no longer needed in the project structure.
- This cleanup helps maintain a tidy codebase by removing unnecessary files that may cause confusion or clutter.
2026-03-04 15:31:45 +09:00
kjs 96637a9cb6 feat: Enhance audit logging with client IP tracking
- Integrated client IP address retrieval in the audit logging functionality across multiple controllers, including admin, common code, department, flow, screen, and table management.
- Updated the `auditLogService` to include a new method for obtaining the client's IP address, ensuring accurate logging of user actions.
- This enhancement improves traceability and accountability by capturing the source of requests, thereby strengthening the overall logging mechanism within the application.
2026-03-04 15:02:27 +09:00
SeongHyun Kim e5abd93600 fix(pop): 카테고리 트리 접기/펼치기 상태를 sessionStorage로 유지
설계 화면에 진입했다 돌아올 때 카테고리 트리와 미분류 회사코드
접기/펼치기 상태가 초기화되는 문제를 수정한다.
expandedGroups, expandedCompanyCodes를 sessionStorage에 저장하여
같은 탭 세션 내에서 상태가 유지되도록 변경.
2026-03-04 14:40:48 +09:00
SeongHyun Kim cc44f714c6 Merge branch 'ksh-v2-work' into main
POP 화면 관리 기능 일괄 병합:
- POP 컴포넌트 연결/상태변경 규칙/후속 액션
- POP 장바구니(CartList) 모드 + 멀티필드 입력
- POP 화면 복사 기능 (단일 + 카테고리 일괄)
- POP 화면관리 UX 개선 (스크롤/접기)
- PC/POP 화면 데이터 분리 (excludePop 필터)
- .gitignore 미사용 항목 정리
충돌 1건 해결 (screenManagementRoutes.ts import 양쪽 통합)
2026-03-04 14:27:46 +09:00
SeongHyun Kim 9b153d85af chore: .gitignore에서 미사용 오케스트레이션 설정 항목 제거
실제 파일이 존재하지 않는 오케스트레이션 관련 gitignore 항목을 정리한다.
(orchestrator.mdc, agents/, commands/, hooks.json, hooks/, plans/)
2026-03-04 14:19:04 +09:00
SeongHyun Kim 37d93d82b1 feat(screen): PC/POP 화면관리 데이터 분리 (excludePop 필터)
PC 화면관리에서 POP 전용 화면과 그룹이 함께 표시되어 혼동을 주는
문제를 해결하기 위해 excludePop 필터를 도입한다.
[백엔드]
- getScreensByCompany: NOT EXISTS 서브쿼리로 screen_layouts_pop에
  레이아웃이 있는 화면 제외, 테이블 별칭 sd로 통일
- getScreenGroups: hierarchy_path 기반으로 POP 그룹 제외
  (hierarchy_path IS NULL OR NOT LIKE 'POP/%')
- 두 API 모두 excludePop 미전달 시 기존 동작 100% 유지
[프론트엔드]
- screenApi.getScreens, getScreenGroups에 excludePop 파라미터 추가
- PC 화면관리 페이지, ScreenGroupTreeView, ScreenList에서
  excludePop: true 전달
2026-03-04 14:01:19 +09:00
kjs 459777d5f0 feat: Update screen management state handling for immediate updates
- Enhanced the screen management functionality to ensure that updates to selected screens are reflected immediately in the state.
- Implemented a mapping function to update the screens list when a screen's details are modified, improving user experience by providing real-time feedback.
- This change ensures that the UI remains in sync with the underlying data, enhancing the overall responsiveness of the application.
2026-03-04 13:53:10 +09:00
kjs b4d5367e2b feat: Integrate audit logging for various operations
- Added audit logging functionality across multiple controllers, including menu, user, department, flow, screen, and table management.
- Implemented logging for create, update, and delete actions, capturing relevant details such as company code, user information, and changes made.
- Enhanced the category tree service with a new endpoint to check if category values are in use, improving data integrity checks.
- Updated routes to include new functionalities and ensure proper logging for batch operations and individual record changes.
- This integration improves traceability and accountability for data modifications within the application.
2026-03-04 13:49:08 +09:00
SeongHyun Kim 15e22ba401 fix(pop): POP 화면 관리 좌측 패널 UX 개선 (스크롤 + 접기/펼치기)
- 좌측 패널 스크롤 수정: 부모에 overflow-hidden, ScrollArea에 min-h-0
  추가하여 미분류 목록이 많을 때 스크롤바가 정상 작동하도록 개선
- 카테고리 그룹 기본 접힌 상태: loadGroups 자동 확장 로직 제거하여
  페이지 진입 시 깔끔한 트리 뷰 제공
- 미분류 회사코드별 접기/펼치기: 최고관리자/COMPANY_7 등 회사코드
  그룹마다 토글 헤더 추가, 항목 수 Badge 표시
2026-03-04 13:18:49 +09:00
SeongHyun Kim ce5c2426b5 feat(pop): POP 화면 복사 기능 구현 (단일 화면 + 카테고리 일괄 복사)
최고관리자의 POP 화면을 다른 회사로 복사하는 기능 추가.
화면 단위 복사와 카테고리(그룹) 단위 일괄 복사를 모두 지원하며,
화면 간 참조(cartScreenId, sourceScreenId 등)를 자동 치환하고
카테고리 구조까지 대상 회사에 재생성한다.
[백엔드]
- analyzePopScreenLinks: POP 레이아웃 내 다른 화면 참조 스캔
- deployPopScreens: screen_definitions + screen_layouts_pop 복사,
  screenId 참조 치환, numberingRuleId 초기화, 그룹 구조 복사
- POP 그룹 조회 쿼리 개선 (screen_layouts_pop JOIN으로 실제 POP 화면만 카운트)
- ensurePopRootGroup 최고관리자 전용으로 변경
[프론트엔드]
- PopDeployModal: 단일 화면/카테고리 일괄 복사 모달 (대상 회사 선택,
  연결 화면 감지, 카테고리 트리 미리보기)
- PopCategoryTree: 그룹 컨텍스트 메뉴에 '카테고리 복사' 추가,
  하위 그룹 화면까지 재귀 수집
- PopScreenSettingModal: UI 간소화 및 화면명 저장 기능 보완
- screenApi: analyzePopScreenLinks, deployPopScreens 클라이언트 함수 추가
2026-03-04 11:41:31 +09:00
DDD1542 c22b468599 feat: Enhance approval request modal functionality
- Added user search capability with debouncing to improve performance and user experience.
- Updated approver management to utilize user data, including user ID, name, position, and department.
- Refactored local ID generation for approvers to a more concise function.
- Integrated approval request handling in button actions, allowing for modal opening with relevant data.
- Improved UI elements for better clarity and user guidance in the approval process.

Made-with: Cursor
2026-03-04 11:19:57 +09:00
DDD1542 6a30038785 fix: baseline TS 에러 정리 및 런타임 에러 수정
- SelectItem value="" -> "none" 변환 (shadcn Select 런타임 에러 수정)
- TablesPanel 중복 React key 수정
- 하이픈 포함 식별자를 따옴표로 감싸기 (hero-section, card-layout)
- 깨진 레거시 파일 제거 (AutoRegisteringLayoutRenderer.ts)
- 중복 인터페이스 통합 (RestAPISourceNodeData, FlowVisibilityConfig)
- WebType에 누락된 타입 추가 (checkbox-group, radio-horizontal 등)
- 사용하지 않는 namespace 제거 (Migration, TypeValidation)
- tsconfig.json에 깨진 레거시 파일 exclude 추가

Made-with: Cursor
2026-03-04 01:13:33 +09:00
DDD1542 89af350935 [agent-pipeline] pipe-20260303124213-d7zo round-4 2026-03-03 22:00:52 +09:00
DDD1542 d9d18c1922 [agent-pipeline] pipe-20260303124213-d7zo round-3 2026-03-03 21:53:46 +09:00
DDD1542 0d71e79c54 [agent-pipeline] pipe-20260303124213-d7zo round-2 2026-03-03 21:49:56 +09:00
SeongHyun Kim 6c9e35e8b2 fix: POP 뷰어 화면 하단 회색 영역 제거
스크롤 컨테이너(overflow-auto)에 배경색이 없어 부모의 bg-gray-100이
컨텐츠 하단 빈 공간에 노출되는 문제 수정.
스크롤 컨테이너에 직접 bg-white를 적용하여 자식 높이 계산과
무관하게 흰색 배경이 유지되도록 변경.
프리뷰 모드에서는 디바이스 프레임 효과를 위해 기존 회색 배경 유지.
2026-03-03 18:57:59 +09:00
SeongHyun Kim 94a541fc9c fix(pop-cart): cart_type 빈 문자열 저장 버그 수정 ("pop" 복원)
- cartItemToDbRecord에서 cart_type: "" -> "pop"으로 복원
- loadFromDb 필터에 cart_type: "pop" 조건 추가하여 POP 데이터만 로드
- 리팩토링(9aaf0575) 중 누락된 값 복원
2026-03-03 17:46:50 +09:00
DDD1542 d7ef26d679 Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into gbpark-node
; 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-03-03 17:13:22 +09:00
SeongHyun Kim 2e8300bbf5 feat(pop): 후속 액션 화면 이동 구현 + 입고확정 버튼 선택 상태 피드백
- PopViewerWithModals에 __pop_navigate__ 이벤트 구독 추가
  - targetScreenId가 있으면 해당 POP 화면으로 이동
  - "back"이면 router.back(), params는 쿼리스트링 전달
- 입고확정 버튼에 카드리스트 선택 상태 시각 피드백
  - 미선택: 기본 아이콘/색상
  - 선택됨: emerald-600 배경 + 선택 개수 뱃지
- selected_items connectionMeta category를 "event"로 변경하여 자동 매칭 대상 포함
2026-03-03 17:13:01 +09:00
DDD1542 65d5392c26 . 2026-03-03 16:58:02 +09:00
SeongHyun Kim f12fca46be fix(pop): 연결 역방향 라우팅 + 확정 후속 액션 + API URL 수정
- useConnectionResolver: _auto 모드에서 역방향(타겟→소스) 라우팅 추가
- pop-button: 입고 확정 성공 후 followUpActions 실행 (navigate/refresh/event)
- pop-button: execute-action API URL 경로 수정 (/api/pop/ → /pop/)
2026-03-03 16:19:22 +09:00
kjs f04d224b09 feat: Enhance error handling with showErrorToast utility
- Replaced existing toast error messages with the new `showErrorToast` utility across multiple components, improving consistency in error reporting.
- Updated error messages to provide more specific guidance for users, enhancing the overall user experience during error scenarios.
- Ensured that all relevant error handling in batch management, external call configurations, cascading management, and screen management components now utilizes the new utility for better maintainability.
2026-03-03 16:04:11 +09:00
kjs ce8b4ed688 feat: Add menu icon support in menu management
- Enhanced the menu management functionality by adding a new `menu_icon` field in the database schema, allowing for the storage of menu icons.
- Updated the `saveMenu` and `updateMenu` functions in the admin controller to handle the new `menu_icon` field during menu creation and updates.
- Modified the `AdminService` to include `MENU_ICON` in various queries, ensuring that the icon data is retrieved and processed correctly.
- Integrated the `MenuIconPicker` component in the frontend to allow users to select and display menu icons in the `MenuFormModal`.
- Updated the sidebar and layout components to utilize the new icon data, enhancing the visual representation of menus across the application.
2026-03-03 15:42:30 +09:00
SeongHyun Kim e3ae8d273c feat(pop): 컴포넌트 연결 단순화 + 상태 변경 규칙 UI 개선 + 조회 키 설정
컴포넌트 연결 단순화
- ConnectionEditor: 이벤트 연결 시 "어디로" Select 1개로 단순화
- useConnectionResolver: 호환 이벤트 자동 라우팅 (_auto 모드)
- connectionMeta에 category(event/filter/data) 필드 추가

상태 변경 규칙 UI 개선
- StatusChangeRule 타입 통합, 모든 버튼 프리셋에서 사용 가능
- TableCombobox/ColumnCombobox 공용 컴포넌트 추출 (pop-shared/)
- 테이블/컬럼 드롭다운, 고정값/조건부 값 설정 UI
- 입고확정 API 신규 (popActionRoutes.ts, 동적 상태 변경 처리)

조회 키 자동/수동 설정
- 대상 테이블 기반 자동 판단 (cart_items -> id, 그 외 -> row_key -> PK)
- 수동 모드: 카드 항목 필드와 대상 PK 컬럼을 직접 지정 가능
- PK 컬럼명 동적 표시 (isPrimaryKey 정보 활용)
2026-03-03 15:31:13 +09:00
kjs f697e1e897 feat: Add procedure and function management in flow controller
- Introduced new endpoints in FlowController for listing procedures and retrieving procedure parameters, enhancing the flow management capabilities.
- Updated FlowDataMoveService to support procedure calls during data movement, ensuring seamless integration with external and internal databases.
- Enhanced NodeFlowExecutionService to execute procedure call actions, allowing for dynamic execution of stored procedures within flow nodes.
- Updated frontend components to support procedure selection and parameter management, improving user experience in configuring flow steps.
- Added necessary types and API functions for handling procedure-related data, ensuring type safety and clarity in implementation.
2026-03-03 14:33:17 +09:00
kjs fd5c61b12a feat: Implement company code validation in flow management
- Enhanced the FlowController to include user company code validation for flow definitions, ensuring that users can only access and modify flows belonging to their company.
- Updated the FlowDefinitionService to accept company code as a parameter for create, update, and delete operations, enforcing ownership checks.
- Introduced sanitization methods in FlowConditionParser to prevent SQL injection for column and table names.
- Modified the FlowDataMoveService to validate table names and column names during data movement operations, enhancing security.
- Updated the frontend components to support batch data movement with proper validation and error handling.
2026-03-03 10:38:38 +09:00
kjs 7ad17065f0 Merge pull request 'jskim-node' (#396) from jskim-node into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/396
2026-02-28 14:37:09 +09:00
kjs e2d88f01e3 Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-28 14:36:12 +09:00
kjs e16d76936b feat: Enhance V2Repeater and configuration panel with source detail auto-fetching
- Added support for automatic fetching of detail rows from the master data in the V2Repeater component, improving data management.
- Introduced a new configuration option in the V2RepeaterConfigPanel to enable source detail auto-fetching, allowing users to specify detail table and foreign key settings.
- Enhanced the V2Repeater component to handle entity joins for loading data, optimizing data retrieval processes.
- Updated the V2RepeaterProps and V2RepeaterConfig interfaces to include new properties for grouped data and source detail configuration, ensuring type safety and clarity in component usage.
- Improved logging for data loading processes to provide better insights during development and debugging.
2026-02-28 14:33:18 +09:00
DDD1542 a8ad26cf30 refactor: Enhance horizontal label handling in dynamic components
- Updated the InteractiveScreenViewerDynamic and RealtimePreviewDynamic components to improve horizontal label rendering and style management.
- Refactored the DynamicComponentRenderer to support external horizontal labels, ensuring proper display and positioning based on component styles.
- Cleaned up style handling by removing unnecessary border properties for horizontal labels, enhancing visual consistency.
- Improved the logic for determining label display requirements, streamlining the rendering process for dynamic components.
2026-02-27 15:24:55 +09:00
SeongHyun Kim 220e05d2ae fix(pop-card-list): 원본 컴포넌트 선택 안 되는 문제 수정
- CartListModeConfig에 sourceComponentId 추가
- cartType이 빈 문자열인 경우 componentId로 매칭
- Select value: sourceComponentId 기반 안정적 매칭
- 런타임: sourceComponentId > cartType > 첫 번째 순으로 폴백

원인: 4160 화면의 cartAction에 cartType 미설정 -> 빈 문자열 ->
Select 저장 시 undefined 순환 -> 선택 불가
2026-02-27 15:16:37 +09:00
SeongHyun Kim c1cf31f57b fix(pop-card-list): 장바구니 모드 설정 UI 개선 + 버그 5건 수정
- screenApi 필드명 수정 (screenId/screenName)
- 레이아웃 components Record -> Object.values() 변환
- config 키 수정 (props -> config)
- SelectItem 빈 값 방어 (cartType || __comp_id)
- cartType 수동입력 -> 원본 컴포넌트 Select 자동 로드
- 상태 필터 UI 제거 (in_cart 고정)
- 설정 미완료 가드 완화 (sourceScreenId만 필수)
2026-02-27 15:13:49 +09:00
SeongHyun Kim aa319a6bda feat(pop-card-list): 장바구니 목록 모드 (cartListMode) 구현
- CartListModeConfig 타입 추가 (sourceScreenId, cartType, statusFilter)
- 원본 화면 카드 설정 자동 상속 (screenApi.getLayoutPop)
- cart_items 조회 + row_data JSON 파싱 + __cart_ 접두사 병합
- 체크박스 선택 (전체/개별) + selected_items 이벤트 발행
- 인라인 삭제 (확인 후 즉시 DB 반영)
- 수량 수정 (NumberInputModal 재사용, 로컬 __cart_modified)
- 설정 패널: 장바구니 모드 토글 + 원본 화면/컴포넌트 선택
- connectionMeta: selected_items, confirm_trigger 추가
2026-02-27 14:57:24 +09:00
DDD1542 026e99511c refactor: Enhance label display and drag-and-drop functionality in table configuration
- Updated the InteractiveScreenViewer and InteractiveScreenViewerDynamic components to include label positioning and size adjustments based on horizontal label settings.
- Improved the DynamicComponentRenderer to handle label display logic more robustly, allowing for string values in addition to boolean.
- Introduced drag-and-drop functionality in the TableListConfigPanel for reordering selected columns, enhancing user experience and flexibility in column management.
- Refactored the display name resolution logic to prioritize available column labels, ensuring accurate representation in the UI.
2026-02-27 14:30:31 +09:00
DDD1542 21c0c2b95c fix: Enhance layout loading logic in screen management
- Updated the ScreenManagementService to allow SUPER_ADMIN or users with companyCode as "*" to load layouts based on the screen's company code.
- Improved layout loading in ScreenViewPage and EditModal components by implementing fallback mechanisms to ensure a valid layout is always set.
- Added console warnings for better debugging when layout loading fails, enhancing error visibility and user experience.
- Refactored label display logic in various components to ensure consistent behavior across input types.
2026-02-27 14:00:06 +09:00
DDD1542 1a6d78df43 refactor: Improve existing item ID handling in BomItemEditorComponent
- Updated the logic for tracking existing item IDs to prevent duplicates during item addition, ensuring that sibling items are checked for duplicates at the same level while allowing duplicates in child levels.
- Enhanced the existingItemIds calculation to differentiate between root level and child level additions, improving data integrity and user experience.
- Refactored the useMemo hook to include addTargetParentId as a dependency, ensuring accurate updates when the target parent ID changes.
2026-02-27 13:30:57 +09:00
kjs b1831ada04 Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-27 13:09:46 +09:00
DDD1542 649bd77bbb feat: Enhance dynamic form and BOM item editor functionality
- Added support for updating the `updated_date` field in the DynamicFormService, ensuring accurate timestamp management.
- Refactored the BomItemEditorComponent to improve data handling by filtering valid fields before saving, enhancing data integrity.
- Introduced a mechanism to track existing item IDs to prevent duplicates during item addition, improving user experience and data consistency.
- Streamlined the save process in ButtonActionExecutor by reorganizing the event handling logic, ensuring better integration with EditModal components.
2026-02-27 13:09:20 +09:00
kjs 8bfc2ba4f5 feat: Enhance dynamic form service to handle VIEW tables
- Introduced a new method `resolveBaseTable` to determine the original table name for VIEWs, allowing for seamless data operations.
- Updated existing methods (`saveFormData`, `updateFormDataPartial`, `updateFormData`, and `deleteFormData`) to utilize `resolveBaseTable`, ensuring that operations are performed on the correct base table.
- Improved logging to provide clearer insights into the operations being performed, including handling of original table names when dealing with VIEWs.
2026-02-27 13:00:22 +09:00
SeongHyun Kim 7bf20bda14 feat(pop-field): 섹션 기반 멀티필드 입력 컴포넌트 구현
pop-field 컴포넌트 전체 구현 (types, component, config, registry):
- 6개 서브타입 (text/number/date/select/auto/numpad)
- 섹션 기반 레이아웃 (summary/form, auto 열 수 계산)
- 저장 탭 5섹션 (테이블/읽기/입력/숨은/자동생성 필드)
- 자동생성 레이아웃 연결 (linkedFieldId)
- 숨은 필드 DB/JSON 소스 선택 (JsonKeySelect 재사용)
- 채번 규칙 API 연동 (getAvailableNumberingRulesForScreen)
- 저장 탭 섹션별 접기/펼치기 토글
- 팔레트 4곳 등록 + index.ts 활성화
2026-02-27 12:48:33 +09:00
kjs c1f7f27005 fix: Improve option filtering in V2Select component
- Updated the option filtering logic to handle null and undefined values, preventing potential crashes when cmdk encounters these values.
- Introduced a safeOptions variable to ensure that only valid options are processed in the dropdown and command list.
- Enhanced the setOptions function to sanitize fetched options, ensuring that only valid values are set, improving overall stability and user experience.
2026-02-27 12:06:49 +09:00
DDD1542 c86337832a Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into gbpark-node
; 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-02-27 11:57:36 +09:00
DDD1542 d686c385e0 feat: Implement edit mode detection in SelectedItemsDetailInputComponent
- Added logic to detect edit mode based on URL parameters and existing data IDs.
- Enhanced value retrieval for form fields to prioritize original data in edit mode, ensuring accurate updates.
- Removed redundant edit mode detection comments to streamline the code and improve clarity.
2026-02-27 11:57:21 +09:00
kjs 0f52c3adc2 refactor: Improve V2Repeater integration and event handling
- Updated the EditModal component to check for registered V2Repeater instances before saving detail data, enhancing the reliability of the repeater save process.
- Simplified the V2Repeater component by removing unnecessary groupedData handling, ensuring it manages its own data effectively.
- Enhanced the DynamicComponentRenderer to correctly handle V2Repeater's data management, improving overall component behavior.
- Refactored button actions to wait for V2Repeater save completion only when active repeaters are present, optimizing performance and user experience.
2026-02-27 11:46:43 +09:00
DDD1542 36bc33860f Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into gbpark-node
; 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-02-27 11:39:37 +09:00
kjs 1b7163ee1a refactor: Hide selected rows information in TableListComponent
- Removed the display of selected rows count and the deselect button from the TableListComponent.
- Updated the comment to indicate that the selected information is now hidden, improving code clarity and maintainability.
2026-02-27 11:01:22 +09:00
DDD1542 c0df38c7ba Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into gbpark-node
; 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-02-27 10:39:51 +09:00
DDD1542 4e997ae36b feat: Enhance V2Select component with automatic value normalization and update handling
- Implemented automatic normalization of legacy plain text values to category codes, improving data consistency.
- Added logic to handle comma-separated values, allowing for better processing of complex input formats.
- Integrated automatic updates to the onChange handler when the normalized value differs from the original, ensuring accurate data saving.
- Updated various select components to utilize the resolved value for consistent behavior across different selection types.
2026-02-27 08:48:21 +09:00
kjs 929b68299a feat: Implement BOM Excel upload and download functionality
- Added endpoints for uploading BOM data from Excel and downloading BOM data in Excel format.
- Developed the `createBomFromExcel` function to handle Excel uploads, including validation and error handling.
- Implemented the `downloadBomExcelData` function to retrieve BOM data for Excel downloads.
- Created a new `BomExcelUploadModal` component for the frontend to facilitate Excel file uploads.
- Updated BOM routes to include new Excel upload and download routes, enhancing BOM management capabilities.
2026-02-27 07:50:22 +09:00
DDD1542 bfc89501ba feat: Enhance BOM and UI components with improved label handling and data mapping
- Updated the BOM service to include additional fields in the BOM header retrieval, enhancing data richness.
- Enhanced the EditModal to automatically map foreign key fields to dot notation, improving data handling and user experience.
- Improved the rendering of labels in various components, allowing for customizable label positions and styles, enhancing UI flexibility.
- Added new properties for label positioning and spacing in the V2 component styles, allowing for better layout control.
- Enhanced the BomTreeComponent to support additional data mapping for entity joins, improving data accessibility and management.
2026-02-27 07:33:54 +09:00
kjs d50f705c44 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-26 20:55:56 +09:00
kjs 708a0fbd1f Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-26 20:55:15 +09:00
kjs bbbdd31311 Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-26 20:55:03 +09:00
kjs 38ade7562e refactor: Update ProcessWorkStandard component to manage work item selection by phase
- Removed the "정보조회" option from the default configuration.
- Refactored the ProcessWorkStandardComponent to handle work item selection independently for each phase.
- Updated the WorkPhaseSection to pass phase-specific parameters for work item selection and detail management.
- Enhanced the useProcessWorkStandard hook to maintain separate states for selected work items and details by phase, improving data handling and user experience.
2026-02-26 20:49:25 +09:00
DDD1542 385a10e2e7 feat: Add BOM version initialization feature and enhance version handling
- Implemented a new endpoint to initialize BOM versions, automatically creating the first version and updating related details.
- Enhanced the BOM service to include logic for version name handling and duplication checks during version creation.
- Updated the BOM controller to support the new initialization functionality, improving BOM management capabilities.
- Improved the BOM version modal to allow users to specify version names during creation, enhancing user experience and flexibility.
2026-02-26 20:48:56 +09:00
kjs 2335a413cb Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-26 17:32:40 +09:00
kjs e622013b3d feat: Enhance image handling in TableCellImage component
- Updated the TableCellImage component to support multiple image inputs, displaying a representative image when available.
- Implemented a new helper function `loadImageBlob` for loading images from blob URLs, improving image loading efficiency.
- Refactored image loading logic to handle both single and multiple objid cases, ensuring robust error handling and loading states.
- Enhanced user experience by allowing direct URL usage for non-objid image paths.
2026-02-26 17:32:39 +09:00
kjs 17d4cc297c feat: Introduce new date picker components for enhanced date selection
- Added `FormDatePicker` and `InlineCellDatePicker` components to provide flexible date selection options.
- Implemented a modernized date picker interface with calendar navigation, year selection, and time input capabilities.
- Enhanced `DateWidget` to support both date and datetime formats, improving user experience in date handling.
- Updated `CategoryColumnList` to group columns dynamically and manage expanded states for better organization.
- Improved `AlertDialog` z-index for better visibility in modal interactions.
- Refactored `ScreenModal` to ensure consistent modal behavior across the application.
2026-02-26 17:32:20 +09:00
DDD1542 afc66a4971 feat: Enhance SelectedItemsDetailInputComponent with improved FK mapping and performance optimizations
- Implemented automatic detection of sourceKeyField based on component configuration, enhancing data handling flexibility.
- Updated SelectedItemsDetailInputConfigPanel to support automatic FK detection and mapping, streamlining configuration.
- Improved database connection logic for DATE types to prevent timezone-related issues.
- Optimized memoization and state management for better overall component performance and user experience.
2026-02-26 17:07:53 +09:00
SeongHyun Kim c161957cfe Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node 2026-02-26 16:53:51 +09:00
SeongHyun Kim 0e0d433ce3 Merge branch 'ksh-v2-work' into main
pop 컴포넌트 중간 병합:
- feat(pop-card-list): PopCardList 컴포넌트 구현 + 3섹션 분리 + 포장 2단계 계산기
- feat(pop-cart): 장바구니 저장 시스템 구현 + 선택적 컬럼 저장
- feat(pop-search): 모달 뷰 개선 (아이콘 뷰, 가나다/ABC 필터 탭)
- feat(pop-button): 버튼 컴포넌트 확장
- fix(pop): 전수 점검 방어 코드 추가
2026-02-26 16:53:21 +09:00
kmh 95c8148787 Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-26 16:51:12 +09:00
DDD1542 52d95b4798 123 2026-02-26 16:50:41 +09:00
DDD1542 43ead0e7f2 feat: Enhance SelectedItemsDetailInputComponent with sourceKeyField auto-detection and FK mapping
- Implemented automatic detection of sourceKeyField based on component configuration, improving flexibility in data handling.
- Enhanced the SelectedItemsDetailInputConfigPanel to support automatic FK detection and mapping, streamlining the configuration process.
- Updated the database connection logic to handle DATE types correctly, preventing timezone-related issues.
- Improved overall component performance by optimizing memoization and state management for better user experience.
2026-02-26 16:39:06 +09:00
kmh 935c737fe3 Merge origin/jskim-node into jskim-node
Made-with: Cursor
2026-02-26 16:29:10 +09:00
kmh 5888ff9c9e Merge branch 'feature/v2-renewal' into jskim-node
Made-with: Cursor
2026-02-26 16:26:48 +09:00
kjs 27be48464a Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-26 16:07:16 +09:00
kjs 20167ad359 feat: Implement advanced filtering capabilities in entity search
- Added a new helper function `applyFilters` to handle dynamic filter conditions for entity search queries.
- Enhanced the `getDistinctColumnValues` and `getEntityOptions` endpoints to support JSON array filters, allowing for more flexible data retrieval based on specified conditions.
- Updated the frontend components to integrate filter conditions, improving user interaction and data management in selection components.
- Introduced new filter options in the V2Select component, enabling users to define and apply various filter criteria dynamically.
2026-02-26 16:07:15 +09:00
SeongHyun Kim 0ca031282b feat(pop-cart): 장바구니 저장 시스템 구현 + 선택적 컬럼 저장
장바구니 담기 -> DB 저장 전체 플로우 구현 및 검증 완료.

- useCartSync 훅 신규: DB(cart_items) <-> 로컬 상태 동기화, dirty check, 일괄 저장
- pop-button cart 프리셋: 배지 표시, 저장 트리거, 확인 모달, 3색 데이터 흐름 시각화
- pop-card-list: 담기/취소 UI, cart_save_trigger 수신 시 saveToDb 실행
- 선택적 컬럼 저장: RowDataMode(all/selected) + 연결 기반 자동 컬럼 로딩
- ComponentEditorPanel: allComponents/connections/componentId를 ConfigPanel에 전달
- connectionMeta: cart_save_trigger/cart_updated/cart_save_completed 이벤트 정의
- ConnectionEditor: 이벤트 타입 연결 구분 (데이터 vs 이벤트)
- types.ts: CartItemWithId, CartSyncStatus, CartButtonConfig 등 타입 추가
- 접근성: NumberInputModal/PackageUnitModal에 DialogTitle 추가

Made-with: Cursor
2026-02-26 16:00:07 +09:00
kjs f90bf63354 Merge pull request 'feat: Add category reference to ColumnSettings interface' (#395) from jskim-node into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/395
2026-02-26 13:53:36 +09:00
kjs 95caa2d10c Merge branch 'main' into jskim-node 2026-02-26 13:53:28 +09:00
kjs 63d8e17392 feat: Add category reference to ColumnSettings interface
- Introduced a new optional property `categoryRef` to the `ColumnSettings` interface in `tableManagement.ts`, allowing for better handling of category references in table configurations.
2026-02-26 13:53:12 +09:00
kjs 52389292a7 Merge pull request 'jskim-node' (#394) from jskim-node into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/394
2026-02-26 13:48:07 +09:00
kjs dd86d5e63c Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-26 13:47:28 +09:00
kjs 495594913f Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-26 13:46:57 +09:00
kjs efc4768ed7 Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-26 13:45:56 +09:00
kmh 4e81571f2b Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-26 13:42:02 +09:00
DDD1542 46ea3612fd feat: Enhance BOM management with new header retrieval and version handling
- Added a new endpoint to retrieve BOM headers with entity join support, improving data accessibility.
- Updated the BOM service to include logic for fetching current version IDs and handling version-related data more effectively.
- Enhanced the BOM tree component to utilize the new BOM header API for better data management.
- Implemented version ID fallback mechanisms to ensure accurate data representation during BOM operations.
- Improved the overall user experience by integrating new features for version management and data loading.
2026-02-26 13:09:32 +09:00
kjs eb27f01616 feat: Enhance category column handling and data mapping
- Updated the `getCategoryColumnsByCompany` and `getCategoryColumnsByMenu` functions to exclude reference columns from category column queries, improving data integrity.
- Modified the `TableManagementService` to include `category_ref` in the column management logic, ensuring proper handling of category references during data operations.
- Enhanced the frontend components to support category reference mapping, allowing for better data representation and user interaction.
- Implemented category label conversion in various components to improve the display of category data, ensuring a seamless user experience.
2026-02-26 11:31:49 +09:00
kmh 5cff85d260 Merge branch 'feature/v2-renewal' into jskim-node
Made-with: Cursor
2026-02-26 09:45:41 +09:00
kjs 863ec614f4 feat: Implement layer activation and data transfer enhancements
- Added support for force-activated layer IDs in ScreenViewPage, allowing layers to be activated based on data events.
- Introduced ScreenContextProvider in ScreenModal and EditModal to manage screen-specific data and context.
- Enhanced V2Repeater to register as a DataReceiver, enabling automatic data handling and integration with ScreenContext.
- Improved ButtonPrimaryComponent to support automatic target component discovery and layer activation for data transfers.
- Updated various components to streamline data handling and improve user experience during data transfers and layer management.
2026-02-25 17:40:17 +09:00
SeongHyun Kim 7a97603106 feat(pop-card-list): 3섹션 분리 + 포장 2단계 계산기 + 설정 패널 개편
- 입력 필드/포장등록/담기 버튼 독립 ON/OFF 분리
- NumberInputModal을 4단계 상태 머신으로 재작성
  (수량 -> 포장 수 -> 개당 수량 -> summary)
- 포장 단위 커스텀 지원 (기본 6종 + 디자이너 추가)
- 본문 필드에 계산식 통합 (3-드롭다운 수식 빌더)
- 입력 필드: limitColumn(동적 상한), saveTable/saveColumn(저장 대상)
- 저장 대상 테이블 선택을 TableCombobox로 교체 (검색 가능)
- 다중 정렬 지원 + 하위 호환 (sorts.map 에러 수정)
- GroupedColumnSelect 항상 테이블명 헤더 표시
- 반응형 표시 우선순위 (required/shrink/hidden) 설정
- PackageEntry/CartItem 타입 확장, CardPackageConfig 신규

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-25 17:03:47 +09:00
DDD1542 0f3ec495a5 feat: Add BOM version activation feature and enhance BOM management
- Implemented the `activateBomVersion` function in the BOM controller to allow activation of specific BOM versions.
- Updated BOM routes to include the new activation endpoint for BOM versions.
- Enhanced the BOM service to handle version activation logic, including status updates and BOM header version changes.
- Improved the BOM version modal to support version activation with user confirmation and feedback.
- Added checks to prevent deletion of active BOM versions, ensuring data integrity.
2026-02-25 16:18:46 +09:00
kmh 5e605efa26 Merge branch 'origin/jskim-node' into jskim-node
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-25 15:42:50 +09:00
kjs 55cbd8778a fix: Update import statements for apiClient in BOM components
- Changed the import statement for apiClient in BomDetailEditModal.tsx and BomHistoryModal.tsx to use named import syntax.
- This change ensures consistency in the import style across the BOM components, improving code readability and maintainability.
2026-02-25 15:34:03 +09:00
kjs 66c92bb7b1 feat: Enhance image rendering in SplitPanelLayoutComponent
- Introduced SplitPanelCellImage component for rendering image thumbnails in table cells, supporting both object IDs and file paths.
- Updated formatCellValue function to display images for columns with input type "image".
- Improved loading logic for column input types to accommodate special rendering for images in both SplitPanelLayoutComponent and V2SplitPanelLayoutComponent.
- Enhanced error handling for image loading failures, ensuring a better user experience when images cannot be displayed.
2026-02-25 15:29:04 +09:00
kjs abb31a39bb feat: Add image thumbnail rendering in SplitPanelLayoutComponent
- Introduced SplitPanelCellImage component to handle image rendering for table cells, supporting both object IDs and file paths.
- Enhanced formatCellValue function to display image thumbnails for columns with input type "image".
- Updated column input types loading logic to accommodate special rendering for images in the right panel.
- Improved error handling for image loading failures, ensuring a better user experience when images cannot be displayed.
2026-02-25 15:28:50 +09:00
DDD1542 18cf5e3269 feat: Add BOM management features and enhance BOM tree component
- Integrated BOM routes into the backend for managing BOM history and versions.
- Enhanced the V2BomTreeConfigPanel to include options for history and version table management.
- Updated the BomTreeComponent to support viewing BOM data in both tree and level formats, with modals for editing BOM details, viewing history, and managing versions.
- Improved user interaction with new buttons for accessing BOM history and version management directly from the BOM tree view.
2026-02-25 14:50:51 +09:00
kjs 262221e300 fix: Refine ExcelUploadModal and TableListComponent for improved data handling
- Updated ExcelUploadModal to automatically generate numbering codes when Excel values are empty, enhancing user experience during data uploads.
- Modified TableListComponent to display only the first image in case of multiple images, ensuring clarity in image representation.
- Improved data handling logic in TableListComponent to prevent unnecessary processing of string values.
2026-02-25 14:42:42 +09:00
DDD1542 ed9e36c213 feat: Enhance table data addition with inserted ID response
- Updated the `addTableData` method in `TableManagementService` to return the inserted ID after adding data to the table.
- Modified the `addTableData` controller to log the inserted ID and include it in the API response, improving client-side data handling.
- Enhanced the `BomTreeComponent` to support additional configurations and improve data loading logic.
- Updated the `ButtonActionExecutor` to handle deferred saves with level-based grouping, ensuring proper ID mapping during master-detail saves.
2026-02-25 13:59:51 +09:00
kjs 38dda2f807 fix: Improve TableListComponent and UniversalFormModalComponent for better data handling
- Updated TableListComponent to use flex-nowrap and overflow-hidden for better badge rendering.
- Enhanced UniversalFormModalComponent to maintain the latest formData using a ref, preventing stale closures during form save events.
- Improved data merging logic in UniversalFormModalComponent to ensure accurate updates and maintain original data integrity.
- Refactored buttonActions to streamline table section data collection and merging, ensuring proper handling of modified and original data during save operations.
2026-02-25 13:53:20 +09:00
kjs 60b1ac1442 feat: Enhance numbering rule service with separator handling
- Introduced functionality to extract and manage individual separators for numbering rule parts.
- Added methods to join parts with their respective separators, improving code generation flexibility.
- Updated the numbering rule service to utilize the new separator logic during part processing.
- Enhanced the frontend components to support custom separators for each part, allowing for more granular control over numbering formats.
2026-02-25 12:25:30 +09:00
DDD1542 2b175a21f4 feat: Enhance entity options retrieval with additional fields support
- Updated the `getEntityOptions` function to accept an optional `fields` parameter, allowing clients to specify additional columns to be retrieved.
- Implemented logic to dynamically include extra columns in the SQL query based on the provided `fields`, improving flexibility in data retrieval.
- Enhanced the response to indicate whether extra fields were included, facilitating better client-side handling of the data.
- Added logging for authentication failures in the `AuthGuard` component to improve debugging and user experience.
- Integrated auto-fill functionality in the `V2Select` component to automatically populate fields based on selected entity references, enhancing user interaction.
- Updated the `ItemSearchModal` to support multi-selection of items, improving usability in item management scenarios.
2026-02-25 11:45:28 +09:00
kmh d09daa1503 Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-25 11:16:27 +09:00
kjs 3ca511924e feat: Implement company-specific NOT NULL constraint validation for table data
- Added validation for NOT NULL constraints in the add and edit table data functions, ensuring that required fields are not empty based on company-specific settings.
- Enhanced the toggleColumnNullable function to check for existing NULL values before changing the NOT NULL status, providing appropriate error messages.
- Introduced a new service method to validate NOT NULL constraints against company-specific configurations, improving data integrity in a multi-tenancy environment.
2026-02-24 18:40:36 +09:00
kjs cb4fa2aaba feat: Implement default version management for routing versions
- Added functionality to set and unset default versions for routing items.
- Introduced new API endpoints for setting and unsetting default versions.
- Enhanced the ItemRoutingComponent to support toggling default versions with user feedback.
- Updated database queries to handle default version logic effectively.
- Improved the overall user experience by allowing easy management of routing versions.
2026-02-24 18:22:54 +09:00
SeongHyun Kim b8569c6641 Merge branch 'ksh-v2-work' 2026-02-24 16:41:29 +09:00
kmh 2392dca6fc Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-24 16:27:15 +09:00
shin 8cfd4024e1 feat(pop-card-list): PopCardList 컴포넌트 구현
- PopCardList 컴포넌트 추가 (NumberInputModal, PackageUnitModal 포함)
- ComponentEditorPanel, PopRenderer 충돌 해결 (modals + onRequestResize 통합)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 15:54:57 +09:00
kjs 593eee3a34 Merge pull request 'jskim-node' (#393) from jskim-node into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/393
2026-02-24 15:31:31 +09:00
kjs 0b6c305024 Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-24 15:30:07 +09:00
kjs 9a85343166 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-24 15:28:59 +09:00
kjs 89b7627bcd Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-24 15:28:46 +09:00
kjs 969b53637a Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-24 15:28:42 +09:00
kjs 5ed2d42377 Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-24 15:28:22 +09:00
kjs a6f37fd3dc feat: Enhance SplitPanelLayoutComponent with improved filtering and modal handling
- Updated search conditions to use an object structure with an "equals" operator for better filtering logic.
- Added validation to ensure an item is selected in the left panel before opening the modal, providing user feedback through a toast notification.
- Extracted foreign key data from the selected left item for improved data handling when opening the modal.
- Cleaned up the code by removing unnecessary comments and consolidating logic for clarity and maintainability.
2026-02-24 15:28:21 +09:00
DDD1542 72068d003a refactor: Enhance screen layout retrieval logic for multi-tenancy support
- Updated the ScreenManagementService to prioritize fetching layouts based on layer_id, ensuring that only the default layer is retrieved for users.
- Implemented logic for administrators to re-query layouts based on the screen definition's company_code when no layout is found.
- Adjusted the BomItemEditorComponent to dynamically render table cells based on configuration, improving flexibility and usability in the BOM item editor.
- Introduced category options loading for dynamic cell rendering, enhancing the user experience in item editing.
2026-02-24 15:27:18 +09:00
SeongHyun Kim 3336384434 fix(pop): 전수 점검 방어 코드 추가 - resolveModalWidth NaN 방어, connections useMemo, filterTabs 의존성 안정화
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 13:05:16 +09:00
SeongHyun Kim 1acd9fc3b2 feat(pop-search): 모달 뷰 전면 개선 - 아이콘 뷰, 가나다/ABC 필터 탭, 컬럼 라벨
모달 타입 통합 (modal-table/card/icon-grid -> modal 1종):
- normalizeInputType()으로 레거시 저장값 호환
- 캔버스 모달 모드 완전 제거 (ModalMode, modalCanvasId, returnEvent)
- SearchInputType 9종으로 정리

모달 뷰 실제 구현:
- TableView / IconView 분리 렌더링 (displayStyle 반영)
- 아이콘 뷰: 이름 첫 글자 컬러 카드 + 초성 그룹 헤더
- getIconColor() 결정적 해시 색상 (16색 팔레트)

가나다/ABC 필터 탭:
- ModalFilterTab 타입 + getGroupKey() 한글 초성 추출
- 쌍자음 합침 (ㄲ->ㄱ, ㄸ->ㄷ 등)
- 모달 상단 토글 버튼으로 초성/알파벳 섹션 그룹화

디자이너 설정 개선:
- 컬럼 헤더 라벨 커스터마이징 (columnLabels)
- 필터 탭 활성화 체크박스 (가나다/ABC)
- card 스타일 제거, 정렬 옵션 제거
- 검색 방식 (포함/시작/같음) 유지

시나리오 A 모달 선택 필터링:
- ConnectionEditor 필터 컬럼에 DB 전체 컬럼 표시
- pop-string-list 복수 필터 AND 지원
- useConnectionResolver 페이로드 구조 정규화

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 12:52:29 +09:00
kjs bb7399df07 Merge pull request 'refactor: Update request type in processWorkStandardController to use AuthenticatedRequest' (#392) from jskim-node into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/392
2026-02-24 12:43:16 +09:00
kjs 6b4250b903 Merge branch 'main' into jskim-node 2026-02-24 12:43:08 +09:00
kjs 076184aad2 refactor: Update request type in processWorkStandardController to use AuthenticatedRequest
- Changed the request type from `Request` to `AuthenticatedRequest` in multiple functions within the processWorkStandardController.
- This update ensures that user authentication details are accessible in the request object, enhancing security and functionality for work item management operations.
2026-02-24 12:42:54 +09:00
kjs 19efe4ada5 Merge pull request 'jskim-node' (#391) from jskim-node into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/391
2026-02-24 12:38:41 +09:00
kjs fc96c958ba Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-24 12:37:59 +09:00
kjs 4f6d9a689d feat: Implement process work standard routes and controller
- Added a new controller for managing process work standards, including CRUD operations for work items and routing processes.
- Introduced routes for fetching items with routing, retrieving routings with processes, and managing work items.
- Integrated the new process work standard routes into the main application file for API accessibility.
- Created a migration script for exporting data related to the new process work standard feature.
- Updated frontend components to support the new process work standard functionality, enhancing the overall user experience.
2026-02-24 12:37:33 +09:00
kmh 4ed2fa4d65 Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-24 11:03:33 +09:00
kjs e8c0828d91 feat: Add process work standard component implementation plan
- Introduced a comprehensive implementation plan for the v2-process-work-standard component, detailing the current state analysis, required database tables, API design, and implementation phases.
- Included a structured file organization plan for both frontend and backend components, ensuring clarity in development and integration.
- Updated the V2Repeater component to support new auto-fill functionalities, including parent sequence generation, enhancing data management capabilities.
- Enhanced the V2RepeaterConfigPanel to allow configuration of parent sequence settings, improving user experience in managing data entries.
2026-02-24 10:15:25 +09:00
SeongHyun Kim 9ccd94d927 feat(pop): 컴포넌트 연결 시스템 구현 - 디자이너 설정 기반 검색->리스트 필터링
ConnectionEditor(연결 탭 UI) + useConnectionResolver(런타임 이벤트 라우터)를 추가하여
디자이너가 코드 없이 컴포넌트 간 데이터 흐름을 설정할 수 있도록 구현.
pop-search -> pop-string-list 실시간 필터링(시나리오 2) 검증 완료.

주요 변경:
- ConnectionEditor: 연결 추가/수정/삭제, 복수 컬럼 체크박스, 필터 모드 선택
- useConnectionResolver: connections 기반 __comp_output__/__comp_input__ 자동 라우팅
- connectionMeta 타입 + pop-search/pop-string-list에 sendable/receivable 등록
- PopDataConnection 확장 (sourceOutput, targetInput, filterConfig, targetColumns)
- pop-search 개선: 필드명 자동화, set_value receivable, number 타입, DRY
- pop-string-list: 복수 컬럼 OR 클라이언트 필터 수신
- "데이터" 탭 -> "연결" 탭, UI 용어 자연어화

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 18:45:21 +09:00
SeongHyun Kim 52b217c180 feat(pop-search): 검색 컴포넌트 MVP 구현
- pop-search 컴포넌트 신규 추가 (Component, Config, types, index)
- 입력 타입: text, number, date, date-preset, select, multi-select, combo, modal-table, modal-card, modal-icon-grid, toggle
- 디자이너 팔레트, 레지스트리, 타입, 렌더러 라벨 등록
- 기본 그리드 크기 4x2, labelText/labelVisible 설정 지원
- filter_changed 이벤트 발행 (연결 시스템 미적용, 추후 dataFlow 기반으로 전환 예정)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 17:16:38 +09:00
SeongHyun Kim f6461ae563 feat(pop-string-list): 오버플로우 시스템 개편 - 더보기 점진 확장 + 페이지네이션 모드
- "전체보기" 토글을 "더보기" 점진 확장으로 변경 (loadMoreCount씩 추가, maxExpandRows 상한)
- 페이지네이션 모드 추가: bottom(하단 페이지 표시) / side(좌우 화살표) 스타일 선택
- StepOverflow 설정 UI에 오버플로우 방식 Select + 모드별 분기 설정 추가
- PopRenderer viewer 모드에서 gridTemplateRows minmax(auto) 적용으로 동적 높이 확장 지원

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 15:08:52 +09:00
SeongHyun Kim fc0e913b8a fix(pop-modal): 중첩 모달 닫기 시 모든 모달이 한꺼번에 닫히는 버그 수정
- handleCloseModal(index) -> handleCloseTopModal()로 변경 (최상위 1개만 닫기)
- onOpenChange에 isTopModal 가드 추가 (하위 모달 연쇄 반응 방지)
- onInteractOutside/onEscapeKeyDown에 isTopModal 가드 추가

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 14:03:55 +09:00
SeongHyun Kim df8cbb3e80 feat(pop): 액션 아키텍처 + 모달 시스템 구현 (STEP 0~7)
- executePopAction / usePopAction 훅 신규 생성
- pop-button을 usePopAction 기반으로 리팩토링
- PopModalDefinition 타입 + MODAL_SIZE_PRESETS 정의
- PopDesignerContext 신규 생성 (모달 탭 상태 공유)
- PopDesigner에 모달 탭 UI 추가 (메인 캔버스 / 모달 캔버스 전환)
- PopCanvas에 접이식 ModalSizeSettingsPanel + ModalThumbnailPreview 구현
- PopViewerWithModals 신규 생성 (뷰어 모달 렌더링 + 스택 관리)
- FULL 모달 전체화면 지원 (h-dvh, w-screen, rounded-none)
- pop-string-list 카드 버튼 액션 연동
- pop-icon / SelectedItemsDetailInput lucide import 최적화
- tsconfig skipLibCheck 설정 추가

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 13:54:49 +09:00
kjs f2528fcb39 Merge pull request 'jskim-node' (#390) from jskim-node into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/390
2026-02-23 12:17:51 +09:00
kmh ea610a243a Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-23 11:18:02 +09:00
kmh f2bd7edf7e Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-23 10:49:48 +09:00
kmh 2289c88320 Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node 2026-02-13 14:10:42 +09:00
SeongHyun Kim 51e1392640 feat(pop-string-list): 컬럼 관리 개선 및 런타임 컬럼 전환 구현
[Config 패널]
- STEP 6 리스트 레이아웃에 컬럼 추가/삭제 기능 추가
- 메인 + 조인 컬럼을 전환 후보로 확장 (기존: 조인만)
- 독립 헤더로 추가된 컬럼은 전환 후보에서 자동 제외
- STEP 3 체크 변경 시 STEP 6 순서/조인컬럼/alternateColumns 보존
- STEP 4 조인 삭제 시 listColumns/alternateColumns에서 고아 참조 자동 정리

[런타임 컴포넌트]
- 리스트 헤더에 alternateColumns 전환 UI 추가 (Popover 드롭다운)
- 조인 컬럼명 resolveColumnName 유틸 추가 ("테이블.컬럼" -> "컬럼")
- 카드 모드 텍스트 잘림 수정 (gridTemplateRows: minmax 적용)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 11:27:40 +09:00
SeongHyun Kim 6842a00890 feat(pop-string-list): 리스트 목록 컴포넌트 MVP 구현
테이블 데이터를 리스트/카드 두 가지 모드로 표시하는 pop-string-list 컴포넌트 전체 구현

- 6단계 Stepper 설정 패널 (모드 선택, 헤더/오버플로우, 데이터+컬럼 선택, 조인 설정, 카드/리스트 레이아웃, 필터/정렬)
- 카드 모드: 시각적 그리드 편집기 (드래그 너비/높이 조절, 셀 병합, 셀별 컬럼/스타일 설정)
- 리스트 모드: 드래그앤드롭 컬럼 순서 변경, 너비 조절, 런타임 컬럼 전환 설정
- 조인 설정: Combobox 테이블 검색, 자동 연결 가능 컬럼 발견, 타입 기반 필터링, 가져올 컬럼 선택
- CardColumnJoin에 selectedTargetColumns 필드 추가
- 디자이너 팔레트/에디터/렌더러에 pop-string-list 등록

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 09:03:52 +09:00
SeongHyun Kim 84426b82cf feat(pop-button): pop-button 컴포넌트 MVP 구현
- PopButtonComponent: 5가지 액션 타입(save/delete/api/modal/event) + 후속 액션 체이닝
- PopButtonConfigPanel: 프리셋 기반 설정(save/delete/logout/menu/modal-open/custom)
- 확인 다이얼로그(ConfirmConfig) 선택 기능
- usePopEvent/useDataSource 훅 연동
- PopComponentType에 pop-button 추가, 팔레트 등록

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 14:23:44 +09:00
SeongHyun Kim ae3261d9bc Merge branch 'ksh-v2-work' into ksh-button 2026-02-12 11:27:37 +09:00
shin 6b8d437a22 feat(pop-card-list): PopCardList 컴포넌트 추가
- PopCardListComponent: 카드 리스트 렌더링 컴포넌트 구현
- PopCardListConfig: 카드 리스트 설정 패널 구현
- types.ts: PopCardListProps 타입 정의 추가
- ComponentPalette: 카드 리스트 컴포넌트 팔레트에 등록
- pop-layout.ts: cardList 타입 추가

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 11:09:02 +09:00
SeongHyun Kim dd561f6e7e chore: popdocs 폴더를 git 추적에서 제거 (.gitignore 적용)
로컬 파일은 유지하되 git 추적만 해제.
이후 .gitignore 규칙이 정상 작동하여 popdocs/ 변경이 커밋 대상에 포함되지 않음.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 09:22:07 +09:00
SeongHyun Kim 8c08e7f8e9 fix(pop-dashboard): 집계 함수 설정 유효성 검증 강화 - 문자열 컬럼에 SUM 적용 방지
팀원 pull 후 대시보드 500 에러 발생 원인 해결:
- subType별 허용 집계 함수 제한 (stat-card는 COUNT만)
- SUM/AVG 선택 시 숫자 타입 컬럼만 표시
- 비호환 집계 함수 선택 시 컬럼 자동 초기화
- subType 변경 시 비호환 aggregation 자동 전환 (STEP 7.5)
- chart 모드 groupBy 미설정 경고 추가

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 09:19:17 +09:00
SeongHyun Kim 1d93b65060 docs(popdocs): Phase 0 공통 인프라 구현 기록 업데이트
- sessions/2026-02-11.md: usePopEvent/useDataSource 구현 내용 추가
- CHANGELOG.md: Phase 0 훅 구현 섹션 추가
- STATUS.md: 진행 상태 + 다음 작업 업데이트
- PLAN.md: 현재 상태 문구 업데이트
- README.md: 마지막 대화 요약 동기화
- INDEX.md: 공통 훅 함수/타입 색인 추가

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-11 16:53:52 +09:00
SeongHyun Kim 300542d922 feat(pop): usePopEvent, useDataSource 공통 훅 구현
- usePopEvent: screenId 기반 이벤트 버스 (publish/subscribe/sharedData)
- useDataSource: DataSourceConfig 기반 데이터 CRUD 통합 훅
  - 집계/조인 → SQL 빌더 경로 (대시보드 로직 재사용)
  - 단순 조회 → dataApi 경로
  - save/update/remove CRUD 래핑
- popSqlBuilder: dataFetcher.ts에서 SQL 빌더 로직 추출 (순수 유틸)
- index.ts: 배럴 파일 (재export)

기존 대시보드(dataFetcher.ts) 미수정, 향후 통합 예정

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-11 16:48:56 +09:00
SeongHyun Kim 0ef0332e08 merge: ksh-dashboard를 ksh-v2-work에 병합 (pop-icon + pop-dashboard 통합)
- pop-icon (아이콘 네비게이션) + pop-dashboard (KPI/차트/게이지) 양쪽 기능 통합
- PopComponentType에 pop-icon, pop-dashboard 모두 등록
- PopRenderer: currentScreenId(아이콘) + previewPageIndex(대시보드) 양쪽 props 공존
- ComponentEditorPanel: previewPageIndex/onPreviewPage props 추가 + 스크롤 CSS 개선 유지
- pop-text: isRealtime 조건부 타이머 로직 적용 (ksh-dashboard 개선 채택)
- COMPONENT_TYPE_LABELS에 pop-icon 라벨 추가 (타입 안전성 보정)

충돌 해결 7개 파일, 17개 지점 - 모두 양쪽 의도 보존

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-11 14:48:59 +09:00
SeongHyun Kim 960b1c9946 feat(pop-dashboard): 라벨 정렬 + 페이지 미리보기 + 차트 디자인 개선
- 라벨 정렬(좌/중/우) 기능 추가 (KPI, 차트, 게이지, 통계카드)
- 글자 크기 커스텀 제거 (컨테이너 반응형 자동 적용)
- 페이지별 미리보기 버튼 추가 (디자이너 캔버스에 즉시 반영)
- 아이템 스타일 에디터 접기/펼치기 지원
- 차트 디자인: CartesianGrid, 대각선 X축 라벨, 숫자 약어(K/M), 축 여백
- handleUpdateComponent stale closure 버그 수정 (함수적 setState)
- 디버그 console.log 전량 제거

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-11 14:23:20 +09:00
shin bae50ffda1 fix(pop-text): 시간/날짜 실시간 업데이트 자동 적용 및 라벨 수정
- 실시간 업데이트 스위치 UI 제거 (datetime 타입은 항상 실시간)
- "날짜/시간 설정" → "시간/날짜 설정" 라벨 변경

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-11 11:00:27 +09:00
shin 929cfb2b61 feat(pop): 아이콘 컴포넌트 추가 및 디자이너 UX 개선
- pop-icon.tsx 신규 추가: 아이콘 컴포넌트 구현
- ComponentPalette: 아이콘 컴포넌트 팔레트 추가
- ComponentEditorPanel: 아이콘 편집 패널 추가
- PopRenderer: 아이콘 렌더링 지원
- pop-layout.ts: 아이콘 타입 정의 추가
- pop-text.tsx: 텍스트 컴포넌트 개선
- next.config.mjs: 설정 업데이트

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-11 10:41:30 +09:00
SeongHyun Kim 1116fb350a 디자이너 캔버스 UX 개선: 헤더 제거 + 실제 데이터 렌더링 + 컴포넌트 목록
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 18:04:43 +09:00
SeongHyun Kim 6f45efef03 디자이너 캔버스 UX 개선: 헤더 제거 + 실제 데이터 렌더링 + 컴포넌트 목록
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 18:02:30 +09:00
SeongHyun Kim bd7bf69a99 fix(pop-dashboard): 아이템/모드 레이아웃 수정 및 게이지 설정 버그 수정
설정 패널 버그 수정 (PopDashboardConfig):
- gaugeConfig 스프레드 순서 수정: min/max/target 값이 기존값에 덮어씌워지는 문제 해결
- 스프레드를 먼저 적용 후 변경 필드를 뒤에 배치하여 올바르게 반영

아이템 레이아웃 개선:
- KpiCard/StatCard: items-center justify-center 추가로 셀 내 중앙 정렬
- GaugeItem: SVG를 flex-1 영역에서 반응형 렌더링 (h-full w-auto)
- GaugeItem: preserveAspectRatio로 비율 유지, 라벨/목표값 shrink-0

모드 레이아웃 개선:
- ArrowsMode: 아이템이 전체 영역 사용, 화살표/인디케이터를 overlay로 변경
- ArrowsMode: 화살표 크기 축소 (h-11 -> h-8), backdrop-blur 추가
- AutoSlideMode: 슬라이드 컨테이너를 absolute inset-0으로 전체 영역 활용
- AutoSlideMode: 인디케이터를 하단 overlay로 변경

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 17:18:00 +09:00
SeongHyun Kim 7a71fc6ca7 fix(pop-dashboard): 차트 X/Y축 자동 적용 및 데이터 처리 안정화
설정 패널 간소화:
- 차트 X축/Y축 수동 입력 필드 제거 (자동 적용 안내 문구로 대체)
- groupBy 선택 시 X축 자동, 집계 결과를 Y축(value)으로 자동 매핑

차트 렌더링 개선 (ChartItem):
- PieChart에 카테고리명+값+비율 라벨 표시
- Legend 컴포넌트 추가 (containerWidth 300px 이상 시)
- Tooltip formatter로 이름/값 쌍 표시

데이터 fetcher 안정화 (dataFetcher):
- apiClient(axios) 우선 호출, dashboardApi(fetch) 폴백 패턴 적용
- PostgreSQL bigint/numeric 문자열 -> 숫자 자동 변환 처리
- Recharts가 숫자 타입을 요구하는 문제 해결

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 16:55:34 +09:00
SeongHyun Kim 578cca2687 feat(pop-dashboard): 4가지 아이템 모드 완성 - 설정 UI 추가 및 버그 수정
설정 패널 (PopDashboardConfig):
- groupBy(X축 분류) Combobox 설정 UI 추가
- 차트 xAxisColumn/yAxisColumn 입력 UI 추가
- 통계 카드 카테고리 추가/삭제/편집 인라인 에디터 추가
- 대상 컬럼 Select를 Combobox(검색 가능)로 개선

데이터 처리 버그 수정 (PopDashboardComponent):
- 차트: groupBy 있을 때 xAxisColumn 자동 보정 로직 추가
- 통계 카드: 카테고리별 필터 실제 적용 (기존: 모든 카테고리에 rows.length 동일 입력)
- useCallback 의존성 안정화 (visibleItemIds 문자열 키 사용)
- refreshInterval 최소 5초 강제

데이터 fetcher 방어 로직 (dataFetcher.ts):
- validateDataSourceConfig() 추가: 설정 미완료 시 SQL 전송 차단
- 빈 필터/불완전 조인 건너뜀 처리
- COUNT 컬럼 미선택 시 COUNT(*) 자동 처리
- fetchTableColumns() 이중 폴백 (tableManagementApi -> dashboardApi)

아이템 UI 개선:
- KPI/차트/게이지/통계 카드 패딩 및 폰트 크기 조정
- 작은 셀에서도 라벨/단위/증감율 표시되도록 hidden 제거

기타:
- GridMode MIN_CELL_WIDTH 160 -> 80 축소
- PLAN.MD: 대시보드 4가지 아이템 모드 완성 계획으로 갱신
- STATUS.md: 프로젝트 상태 추적 파일 추가

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 16:12:29 +09:00
SeongHyun Kim dc523d86c3 feat(pop-dashboard): 페이지 기반 구조 전환 및 설정 패널 고도화
구조 변경:
- grid 모드를 독립 displayMode에서 페이지 내부 그리드 레이아웃으로 전환
- DashboardPage 타입 추가 (각 페이지가 독립 그리드 보유)
- migrateConfig()로 기존 grid/useGridLayout 설정 자동 마이그레이션

설정 패널 (PopDashboardConfig):
- 드롭다운 기반 집계 설정 UI 전면 재작성 (+917줄)
- 테이블/컬럼 선택 Combobox, 페이지 관리, 셀 배치 편집기
- fetchTableList() 추가 (테이블 목록 조회)

컴포넌트/모드 개선:
- GridMode: 반응형 자동 열 축소 (MIN_CELL_WIDTH 기준)
- PopDashboardComponent: 페이지 기반 렌더링 로직
- PopDashboardPreview: 페이지 뱃지 표시

기타:
- ComponentEditorPanel: 탭 콘텐츠 스크롤 수정 (min-h-0 추가)
- types.ts: grid를 displayMode에서 제거, DashboardPage 타입 추가

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 14:22:30 +09:00
SeongHyun Kim 73e3d56381 fix(pop-dashboard): React Hooks 규칙 위반 수정 + ConfigPanel props 정합성 + 방어 코드 강화
- PopDashboardComponent: early return을 모든 hooks 이후로 이동 (Rules of Hooks)
- PopDashboardConfigPanel: onChange -> onUpdate prop 이름 정합, 빈 객체 config 방어
- PopDashboardPreview: Array.isArray 방어 추가

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 12:20:44 +09:00
SeongHyun Kim 4f3e9ec19e feat(pop-dashboard): Phase 0 공통 타입 + Phase 1 대시보드 컴포넌트 구현
Phase 0: 공통 인프라 타입 정의
- ColumnBinding, JoinConfig, DataSourceConfig, PopActionConfig 등
- FilterOperator, AggregationType, SortConfig 타입

Phase 1: pop-dashboard 컴포넌트
- 4개 서브타입: KpiCard, ChartItem, GaugeItem, StatCard
- 4개 표시모드: ArrowsMode, AutoSlideMode, GridMode, ScrollMode
- 설정패널(PopDashboardConfig), 미리보기(PopDashboardPreview)
- 계산식 엔진(formula.ts), 데이터 조회(dataFetcher.ts)
- 팔레트/렌더러/타입 시스템 연동

fix(pop-text): DateTimeDisplay isRealtime 기본값 true로 수정
EOF
2026-02-10 11:04:18 +09:00
SeongHyun Kim f825d65bfc feat(pop): 뷰어 스크롤 수정 및 컴포넌트 레지스트리 연동
- page.tsx: overflow-hidden 제거로 뷰어 스크롤 활성화,
  pop-components 레지스트리 자동 등록 import 추가
- PopRenderer.tsx: 레지스트리에서 실제 컴포넌트 조회 후 렌더링,
  미등록 컴포넌트는 플레이스홀더 fallback 표시
- PLAN.MD: POP 뷰어 스크롤 수정 계획으로 업데이트
- POPUPDATE_2.md: v8.0 - 모달 화면 설계 규칙(제9조) 추가,
  버튼 modal action 스펙 확장 (inline/screen-ref 모드)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 19:31:46 +09:00
SeongHyun Kim a017c1352a chore: popdocs 폴더 git 추적 제외
개인 작업 문서(popdocs/)를 .gitignore에 추가하고 추적 해제

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 18:52:33 +09:00
SeongHyun Kim 642f433bcb docs: POP 컴포넌트 정의서 v7.0 추가
- 9개 컴포넌트 정의 (pop-text, pop-dashboard, pop-table, pop-button, pop-icon, pop-search, pop-field, pop-lookup, pop-system)
- POP 헌법 (공통 규칙 8조) 추가
- 공통 인프라 설계 (DataSourceConfig, ColumnBinding, JoinConfig, useDataSource, usePopEvent, PopActionConfig)
- 컴포넌트 간 통신 예시 4가지
- 구현 우선순위 Phase 0~6

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 14:18:40 +09:00
2770 changed files with 86412 additions and 867853 deletions

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="output" path="WebContent/WEB-INF/classes"/>
</classpath>

View File

@ -1,8 +1,8 @@
{ {
"mcpServers": { "mcpServers": {
"agent-orchestrator": { "Framelink Figma MCP": {
"command": "node", "command": "npx",
"args": ["/Users/gbpark/ERP-node/mcp-agent-orchestrator/build/index.js"] "args": ["-y", "figma-developer-mcp", "--figma-api-key=figd_NrYdIWf-CnC23NyH6eMym7sBdfbZTuXyS91tI3VS", "--stdio"]
} }
} }
} }

View File

@ -1,69 +0,0 @@
---
description:
globs:
alwaysApply: true
---
# 아키텍처 가이드
## 전체 아키텍처
이 애플리케이션은 전형적인 Spring MVC 3-tier 아키텍처를 따릅니다:
- **Presentation Layer**: JSP + jQuery (Frontend)
- **Business Layer**: Spring Controllers + Services (Backend Logic)
- **Data Access Layer**: MyBatis + PostgreSQL (Database)
## 패키지 구조
```
src/com/pms/
├── controller/ # Spring MVC Controllers (@Controller)
├── service/ # Business Logic (@Service)
├── mapper/ # MyBatis XML Mappers
├── common/ # 공통 유틸리티 및 설정
├── salesmgmt/ # 영업관리 모듈
└── ions/ # 특수 모듈
```
## 주요 컴포넌트
### Controllers
모든 컨트롤러는 [BaseService](mdc:src/com/pms/common/service/BaseService.java)를 상속받습니다.
- URL 패턴: `*.do` (예: `/admin/menuMngList.do`)
- 주요 컨트롤러: [AdminController](mdc:src/com/pms/controller/AdminController.java)
### Services
비즈니스 로직을 처리하는 서비스 계층입니다.
- 예시: [AdminService](mdc:src/com/pms/service/AdminService.java)
- MyBatis SqlSession을 직접 사용하여 데이터베이스 접근
### MyBatis Mappers
SQL 쿼리를 XML로 정의합니다.
- 위치: `src/com/pms/mapper/`
- 예시: [admin.xml](mdc:src/com/pms/mapper/admin.xml)
### JSP Views
JSP 뷰 파일들은 `WebContent/WEB-INF/view/` 디렉토리에 위치합니다.
- InternalResourceViewResolver 사용
- prefix: `/WEB-INF/view`, suffix: `.jsp`
## 데이터베이스 설정
- JNDI DataSource 사용: `plm`
- PostgreSQL 연결
- 초기 데이터: [ilshin.pgsql](mdc:db/ilshin.pgsql)
## 설정 파일 위치
- Spring 설정: [dispatcher-servlet.xml](mdc:WebContent/WEB-INF/dispatcher-servlet.xml)
- 로깅 설정: [log4j.xml](mdc:WebContent/WEB-INF/log4j.xml)
- 웹 설정: [web.xml](mdc:WebContent/WEB-INF/web.xml)

View File

@ -1,207 +1,145 @@
--- ---
description: description:
globs: globs:
alwaysApply: true alwaysApply: true
--- ---
# 데이터베이스 가이드 # 데이터베이스 가이드
## 데이터베이스 설정 ## 데이터베이스 설정
### PostgreSQL 연결 ### PostgreSQL 연결
- **JNDI 리소스명**: `plm` - **드라이버**: `pg` (node-postgres)
- **드라이버**: `org.postgresql.Driver` - **설정 파일**: `backend-node/src/config/database.ts`
- **설정 파일**: [context.xml](mdc:tomcat-conf/context.xml) - **환경 변수**: `backend-node/.env` (DATABASE_URL)
### 초기 데이터 ### 연결 풀 사용
- **스키마 파일**: [ilshin.pgsql](mdc:db/ilshin.pgsql) ```typescript
- **역할 설정**: [00-create-roles.sh](mdc:db/00-create-roles.sh) import { Pool } from 'pg';
## MyBatis 설정 const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
});
```
### SqlSession 사용 패턴 ## 쿼리 패턴
```java
public List<Map<String, Object>> getData(Map<String, Object> paramMap) { ### 기본 CRUD
SqlSession sqlSession = SqlMapConfig.getInstance().getSqlSession(); ```typescript
try { // SELECT
return sqlSession.selectList("namespace.queryId", paramMap); const result = await pool.query(
} finally { 'SELECT * FROM example_table WHERE company_code = $1 ORDER BY created_at DESC',
sqlSession.close(); // 반드시 리소스 해제 [companyCode]
} );
}
// INSERT
const result = await pool.query(
'INSERT INTO example_table (company_code, name) VALUES ($1, $2) RETURNING *',
[companyCode, name]
);
// UPDATE
const result = await pool.query(
'UPDATE example_table SET name = $1, updated_at = NOW() WHERE id = $2 AND company_code = $3 RETURNING *',
[name, id, companyCode]
);
// DELETE
const result = await pool.query(
'DELETE FROM example_table WHERE id = $1 AND company_code = $2 RETURNING id',
[id, companyCode]
);
``` ```
### 트랜잭션 처리 ### 트랜잭션 처리
```java ```typescript
public void saveData(Map<String, Object> paramMap) { const client = await pool.connect();
SqlSession sqlSession = SqlMapConfig.getInstance().getSqlSession(false); // autoCommit=false try {
try { await client.query('BEGIN');
sqlSession.insert("namespace.insertQuery", paramMap); await client.query('INSERT INTO ...', [params]);
sqlSession.update("namespace.updateQuery", paramMap); await client.query('UPDATE ...', [params]);
sqlSession.commit(); // 명시적 커밋 await client.query('COMMIT');
} catch (Exception e) { } catch (e) {
sqlSession.rollback(); // 오류 시 롤백 await client.query('ROLLBACK');
throw e; throw e;
} finally { } finally {
sqlSession.close(); client.release();
}
} }
``` ```
## 매퍼 XML 작성 가이드 ### 동적 조건 처리
```typescript
const conditions: string[] = ['company_code = $1'];
const params: any[] = [companyCode];
let paramIndex = 2;
### 기본 구조 if (searchText) {
```xml conditions.push(`name ILIKE $${paramIndex}`);
<?xml version="1.0" encoding="UTF-8"?> params.push(`%${searchText}%`);
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" paramIndex++;
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> }
<mapper namespace="admin">
<!-- 쿼리 정의 -->
</mapper>
```
### 파라미터 바인딩 if (status) {
```xml conditions.push(`status = $${paramIndex}`);
<!-- 안전한 파라미터 바인딩 (권장) --> params.push(status);
<select id="selectUser" parameterType="map" resultType="map"> paramIndex++;
SELECT * FROM users }
WHERE user_id = #{userId}
AND status = #{status}
</select>
<!-- 동적 조건 처리 --> const query = `SELECT * FROM example_table WHERE ${conditions.join(' AND ')} ORDER BY created_at DESC`;
<select id="selectUserList" parameterType="map" resultType="map"> const result = await pool.query(query, params);
SELECT * FROM users
WHERE 1=1
<if test="userName != null and userName != ''">
AND user_name LIKE '%' || #{userName} || '%'
</if>
<if test="deptCode != null and deptCode != ''">
AND dept_code = #{deptCode}
</if>
</select>
```
### PostgreSQL 특화 문법
```xml
<!-- 시퀀스 사용 -->
<insert id="insertData" parameterType="map">
INSERT INTO table_name (id, name, reg_date)
VALUES (nextval('seq_table'), #{name}, now())
</insert>
<!-- 숫자 타입 캐스팅 -->
<update id="updateData" parameterType="map">
UPDATE table_name
SET status = #{status}
WHERE id = #{id}::numeric
</update>
<!-- 재귀 쿼리 (메뉴 트리 구조) -->
<select id="selectMenuTree" resultType="map">
WITH RECURSIVE menu_tree AS (
SELECT * FROM menu_info WHERE parent_id = 0
UNION ALL
SELECT m.* FROM menu_info m
JOIN menu_tree mt ON m.parent_id = mt.id
)
SELECT * FROM menu_tree ORDER BY path
</select>
``` ```
## 주요 테이블 구조 ## 주요 테이블 구조
### 메뉴 관리 ### 메뉴 관리
- **MENU_INFO**: 메뉴 정보 - **menu_info**: 메뉴 정보
- **MENU_AUTH_GROUP**: 메뉴 권한 그룹 - **menu_auth_group**: 메뉴 권한 그룹
- **AUTH_GROUP**: 권한 그룹 정보 - **auth_group**: 권한 그룹 정보
### 사용자 관리 ### 사용자 관리
- **USER_INFO**: 사용자 정보 - **user_info**: 사용자 정보
- **DEPT_INFO**: 부서 정보 - **dept_info**: 부서 정보
- **USER_AUTH**: 사용자 권한 - **user_auth**: 사용자 권한
### 코드 관리 ### 코드 관리
- **CODE_INFO**: 공통 코드 - **code_info**: 공통 코드
- **CODE_CATEGORY**: 코드 카테고리 - **code_category**: 코드 카테고리
## 데이터베이스 개발 모범 사례 ## 마이그레이션
### 1. 파라미터 검증 마이그레이션 파일은 `db/migrations/` 디렉토리에 순번으로 관리:
```xml
<select id="selectData" parameterType="map" resultType="map">
SELECT * FROM table_name
WHERE 1=1
<if test="id != null and id != ''">
AND id = #{id}::numeric
</if>
</select>
``` ```
db/migrations/
### 2. 페이징 처리 ├── 001_initial_schema.sql
```xml ├── 002_add_company_code.sql
<select id="selectListWithPaging" parameterType="map" resultType="map"> ├── ...
SELECT * FROM ( └── 1021_create_numbering_audit_log.sql
SELECT *, ROW_NUMBER() OVER (ORDER BY reg_date DESC) as rnum
FROM table_name
WHERE 1=1
<!-- 검색 조건 -->
) t
WHERE rnum BETWEEN #{startRow}::numeric AND #{endRow}::numeric
</select>
```
### 3. 대소문자 처리
```xml
<!-- PostgreSQL은 대소문자를 구분하므로 주의 -->
<select id="selectData" resultType="map">
SELECT
user_id as "userId", -- 카멜케이스 변환
user_name as "userName",
UPPER(status) as "status"
FROM user_info
</select>
```
### 4. NULL 처리
```xml
<select id="selectData" resultType="map">
SELECT
COALESCE(description, '') as description,
CASE WHEN status = 'Y' THEN '활성' ELSE '비활성' END as statusName
FROM table_name
</select>
``` ```
## 성능 최적화 ## 성능 최적화
### 인덱스 활용 ### 인덱스 활용
```sql ```sql
-- 자주 검색되는 컬럼에 인덱스 생성 CREATE INDEX idx_example_company_code ON example_table(company_code);
CREATE INDEX idx_user_dept ON user_info(dept_code); CREATE INDEX idx_user_dept ON user_info(dept_code);
CREATE INDEX idx_menu_parent ON menu_info(parent_id);
``` ```
### 쿼리 최적화 ### 페이징 처리
```xml ```typescript
<!-- EXISTS 사용으로 성능 개선 --> const query = `
<select id="selectUserWithAuth" resultType="map"> SELECT * FROM example_table
SELECT u.* FROM user_info u WHERE company_code = $1
WHERE EXISTS ( ORDER BY created_at DESC
SELECT 1 FROM user_auth ua LIMIT $2 OFFSET $3
WHERE ua.user_id = u.user_id `;
AND ua.auth_code = #{authCode} const result = await pool.query(query, [companyCode, limit, offset]);
)
</select>
``` ```
### 배치 처리 ### 슬로우 쿼리 확인
```xml ```sql
<!-- 대량 데이터 삽입 시 배치 사용 --> SELECT query, mean_time, calls
<insert id="insertBatch" parameterType="list"> FROM pg_stat_statements
INSERT INTO table_name (col1, col2, col3) ORDER BY mean_time DESC;
VALUES ```
<foreach collection="list" item="item" separator=",">
(#{item.col1}, #{item.col2}, #{item.col3})
</foreach>
</insert>
```

View File

@ -1,118 +0,0 @@
---
description:
globs:
alwaysApply: true
---
# 개발 가이드
## 개발 환경 설정
### Docker 개발 환경
```bash
# 개발 환경 실행
docker-compose -f docker-compose.dev.yml up --build -d
# 운영 환경 실행
docker-compose -f docker-compose.prod.yml up --build -d
```
### 로컬 개발 환경
1. Java 7 JDK 설치
2. Eclipse IDE 설정
3. Tomcat 7.0 설정
4. PostgreSQL 데이터베이스 설정
## 코딩 컨벤션
### Controller 개발
```java
@Controller
public class ExampleController extends BaseService {
@Autowired
ExampleService exampleService;
@RequestMapping("/example/list.do")
public String getList(HttpServletRequest request,
@RequestParam Map<String, Object> paramMap) {
// 비즈니스 로직은 Service에서 처리
List<Map<String, Object>> list = exampleService.getList(request, paramMap);
request.setAttribute("list", list);
return "/example/list";
}
}
```
### Service 개발
```java
@Service
public class ExampleService extends BaseService {
public List<Map<String, Object>> getList(HttpServletRequest request,
Map<String, Object> paramMap) {
SqlSession sqlSession = SqlMapConfig.getInstance().getSqlSession();
try {
return sqlSession.selectList("example.selectList", paramMap);
} finally {
sqlSession.close();
}
}
}
```
### MyBatis Mapper 개발
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="example">
<select id="selectList" parameterType="map" resultType="map">
SELECT * FROM example_table
WHERE 1=1
<if test="searchText != null and searchText != ''">
AND name LIKE '%' || #{searchText} || '%'
</if>
</select>
</mapper>
```
## 주요 유틸리티
### 공통 유틸리티
- [CommonUtils](mdc:src/com/pms/common/utils/CommonUtils.java) - 공통 유틸리티 메서드
- [Constants](mdc:src/com/pms/common/utils/Constants.java) - 상수 정의
- [Message](mdc:src/com/pms/common/Message.java) - 메시지 처리
### 파일 관련
- [FileRenameClass](mdc:src/com/pms/common/FileRenameClass.java) - 파일명 변경
- 파일 업로드/다운로드 처리
## 프론트엔드 개발
### JSP 개발
- 위치: `WebContent/WEB-INF/view/`
- 공통 초기화: [init_jqGrid.jsp](mdc:WebContent/init_jqGrid.jsp)
- 스타일시트: [all.css](mdc:WebContent/css/all.css)
### JavaScript 라이브러리
- jQuery 1.11.3/2.1.4
- jqGrid 4.7.1 - 데이터 그리드
- Tabulator - 테이블 컴포넌트
- rMateChart - 차트 라이브러리
## 데이터베이스 개발
### 연결 설정
- JNDI 리소스명: `plm`
- 드라이버: PostgreSQL
- 컨텍스트 설정: [context.xml](mdc:tomcat-conf/context.xml)
### 스키마 관리
- 초기 스키마: [ilshin.pgsql](mdc:db/ilshin.pgsql)
- 역할 설정: [00-create-roles.sh](mdc:db/00-create-roles.sh)
## 빌드 및 배포
- Eclipse 기반 빌드 (Maven/Gradle 미사용)
- 컴파일된 클래스: `WebContent/WEB-INF/classes/`
- 라이브러리: `WebContent/WEB-INF/lib/`
- WAR 파일로 Tomcat 배포

View File

@ -1,176 +0,0 @@
---
description:
globs:
alwaysApply: true
---
# Next.js 마이그레이션 가이드
## 마이그레이션 개요
현재 JSP/jQuery 기반 프론트엔드를 Next.js로 전환하는 작업이 계획되어 있습니다.
자세한 내용은 [TODO.md](mdc:TODO.md)를 참조하세요.
## 현재 프론트엔드 분석
### JSP 뷰 구조
```
WebContent/WEB-INF/view/
├── admin/ # 관리자 화면
├── approval/ # 승인 관리
├── common/ # 공통 컴포넌트
├── dashboard/ # 대시보드
├── main/ # 메인 화면
└── ... # 기타 모듈별 화면
```
### 주요 JavaScript 라이브러리
- **jQuery**: 1.11.3/2.1.4 - DOM 조작 및 AJAX
- **jqGrid**: 4.7.1 - 데이터 그리드 (교체 필요)
- **Tabulator**: 테이블 컴포넌트
- **rMateChart**: 차트 라이브러리 (교체 필요)
- **CKEditor**: 텍스트 에디터
### CSS 프레임워크
- [all.css](mdc:WebContent/css/all.css) - 메인 스타일시트
- jQuery UI 테마 적용
- 반응형 디자인 미적용 (데스크톱 중심)
## API 설계 가이드
### RESTful API 변환
현재 Spring MVC는 JSP 뷰를 반환하는 구조입니다:
```java
@RequestMapping("/admin/menuMngList.do")
public String getMenuList(HttpServletRequest request, @RequestParam Map<String, Object> paramMap) {
// 데이터 조회
List<Map<String, Object>> menuList = adminService.getMenuList(request, paramMap);
request.setAttribute("menuList", menuList);
return "/admin/menu/menuMngList"; // JSP 뷰 반환
}
```
Next.js 연동을 위해 JSON API로 변환 필요:
```java
@RestController
@RequestMapping("/api")
public class AdminApiController {
@GetMapping("/admin/menus")
@ResponseBody
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> getMenuList(
@RequestParam Map<String, Object> paramMap) {
List<Map<String, Object>> menuList = adminService.getMenuList(null, paramMap);
return ResponseEntity.ok(ApiResponse.success(menuList));
}
}
```
### API 응답 표준화
```java
public class ApiResponse<T> {
private boolean success;
private T data;
private String message;
private String errorCode;
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, data, null, null);
}
public static <T> ApiResponse<T> error(String message, String errorCode) {
return new ApiResponse<>(false, null, message, errorCode);
}
}
```
## 컴포넌트 매핑 가이드
### 데이터 그리드 교체
**현재**: jqGrid 4.7.1
```javascript
$("#grid").jqGrid({
url: 'menuMngList.do',
datatype: 'json',
colModel: [
{name: 'menuName', label: '메뉴명'},
{name: 'url', label: 'URL'}
]
});
```
**변환 후**: TanStack Table 또는 AG Grid
```tsx
import { useTable } from '@tanstack/react-table';
const MenuTable = () => {
const columns = [
{ accessorKey: 'menuName', header: '메뉴명' },
{ accessorKey: 'url', header: 'URL' }
];
const table = useTable({ data, columns });
// 테이블 렌더링
};
```
### 차트 라이브러리 교체
**현재**: rMateChart
**변환 후**: Recharts 또는 Chart.js
### 폼 처리 교체
**현재**: jQuery 기반 폼 처리
**변환 후**: react-hook-form 사용
## 인증/인가 처리
### 현재 세션 기반 인증
```java
// 세션에서 사용자 정보 조회
PersonBean person = (PersonBean)request.getSession().getAttribute(Constants.PERSON_BEAN);
```
### Next.js 연동 방안
1. **세션 유지**: 쿠키 기반 세션 ID 전달
2. **JWT 토큰**: 새로운 토큰 기반 인증 도입
3. **하이브리드**: 기존 세션 + API 토큰
## 개발 단계별 접근
### Phase 1: API 개발
1. 기존 Controller 분석
2. @RestController 신규 생성
3. 기존 Service 재사용
4. CORS 설정 추가
### Phase 2: Next.js 기본 구조
1. Next.js 프로젝트 생성
2. 기본 레이아웃 구현
3. 라우팅 구조 설계
4. 공통 컴포넌트 개발
### Phase 3: 화면별 마이그레이션
1. 관리자 화면부터 시작
2. 주요 업무 화면 순차 전환
3. 대시보드 및 리포트 화면
### Phase 4: 테스트 및 최적화
1. 기능 테스트
2. 성능 최적화
3. 사용자 테스트
4. 점진적 배포
## 주의사항
### 데이터 호환성
- 기존 데이터베이스 스키마 유지
- API 응답 형식 표준화
- 날짜/시간 형식 통일
### 사용자 경험
- 기존 업무 프로세스 유지
- 화면 전환 시 혼란 최소화
- 점진적 마이그레이션 고려
### 성능 고려사항
- API 응답 속도 최적화
- 클라이언트 사이드 캐싱
- 이미지 및 정적 자원 최적화

View File

@ -1,103 +0,0 @@
---
description:
globs:
alwaysApply: true
---
# 네비게이션 가이드
## 프로젝트 구조 이해
### 루트 디렉토리
```
plm-ilshin/
├── src/ # Java 소스 코드
├── WebContent/ # 웹 리소스 (JSP, CSS, JS, 이미지)
├── db/ # 데이터베이스 스크립트
├── tomcat-conf/ # Tomcat 설정
├── docker-compose.*.yml # Docker 설정
└── 문서/ # 프로젝트 문서
```
### 주요 소스 디렉토리
```
src/com/pms/
├── controller/ # 웹 컨트롤러 (@Controller)
├── service/ # 비즈니스 로직 (@Service)
├── mapper/ # MyBatis SQL 매퍼 (XML)
├── common/ # 공통 컴포넌트
├── salesmgmt/ # 영업관리 모듈
└── ions/ # 특수 기능 모듈
```
### 웹 리소스 구조
```
WebContent/
├── WEB-INF/
│ ├── view/ # JSP 뷰 파일
│ ├── lib/ # JAR 라이브러리
│ ├── classes/ # 컴파일된 클래스
│ └── *.xml # 설정 파일
├── css/ # 스타일시트
├── js/ # JavaScript 파일
├── images/ # 이미지 리소스
└── template/ # 템플릿 파일
```
## 주요 파일 찾기
### 컨트롤러 찾기
특정 URL에 대한 컨트롤러를 찾을 때:
1. URL 패턴 확인 (예: `/admin/menuMngList.do`)
2. `src/com/pms/controller/` 에서 해당 `@RequestMapping` 검색
3. 주요 컨트롤러들:
- [AdminController.java](mdc:src/com/pms/controller/AdminController.java) - 관리자 기능
- [ApprovalController.java](mdc:src/com/pms/controller/ApprovalController.java) - 승인 관리
- [AsController.java](mdc:src/com/pms/controller/AsController.java) - AS 관리
### 서비스 찾기
비즈니스 로직을 찾을 때:
1. 컨트롤러에서 `@Autowired` 된 서비스 확인
2. `src/com/pms/service/` 디렉토리에서 해당 서비스 파일 찾기
3. 주요 서비스들:
- [AdminService.java](mdc:src/com/pms/service/AdminService.java) - 관리자 서비스
- [ApprovalService.java](mdc:src/com/pms/service/ApprovalService.java) - 승인 서비스
### SQL 쿼리 찾기
데이터베이스 쿼리를 찾을 때:
1. 서비스 코드에서 `sqlSession.selectList("namespace.queryId")` 확인
2. `src/com/pms/mapper/` 에서 해당 namespace XML 파일 찾기
3. XML 파일 내에서 queryId로 검색
### JSP 뷰 찾기
화면을 찾을 때:
1. 컨트롤러 메서드의 return 값 확인 (예: `"/admin/menu/menuMngList"`)
2. `WebContent/WEB-INF/view/` + return 값 + `.jsp` 경로로 파일 찾기
## 모듈별 주요 기능
### 관리자 모듈 (`/admin/*`)
- 메뉴 관리: [AdminController.java](mdc:src/com/pms/controller/AdminController.java)
- 사용자 관리, 권한 관리
- 코드 관리, 카테고리 관리
- 시스템 로그 관리
### 영업 관리 (`/salesmgmt/*`)
- 위치: `src/com/pms/salesmgmt/`
- 영업 관련 컨트롤러, 서비스, 매퍼 분리
### 공통 기능 (`/common/*`)
- 공통 유틸리티: [CommonUtils](mdc:src/com/pms/common/utils/CommonUtils.java)
- 메시지 처리: [Message](mdc:src/com/pms/common/Message.java)
- 파일 처리: [FileRenameClass](mdc:src/com/pms/common/FileRenameClass.java)
## 개발 시 주의사항
### 파일 수정 시
1. Java 파일 수정 → Eclipse에서 자동 컴파일 → `WebContent/WEB-INF/classes/`에 반영
2. JSP/CSS/JS 수정 → 바로 반영 (서버 재시작 불필요)
3. XML 설정 파일 수정 → 서버 재시작 필요
### 데이터베이스 관련
1. 스키마 변경 시 [ilshin.pgsql](mdc:db/ilshin.pgsql) 업데이트
2. 새로운 쿼리 추가 시 해당 mapper XML 파일에 추가
3. 트랜잭션 처리는 서비스 레벨에서 관리

View File

@ -8,30 +8,69 @@ alwaysApply: true
## 프로젝트 정보 ## 프로젝트 정보
이 프로젝트는 제품 수명 주기 관리(PLM - Product Lifecycle Management) 솔루션입니다. 이 프로젝트는 WACE ERP/PLM 솔루션입니다.
Spring Framework 기반의 Java 웹 애플리케이션으로, 제품 개발부터 폐기까지의 전체 생명주기를 관리합니다. Node.js + Next.js 기반의 풀스택 웹 애플리케이션으로, 멀티테넌시를 지원합니다.
## 기술 스택 ## 기술 스택
- **Backend**: Java 7, Spring Framework 3.2.4, MyBatis 3.2.3 - **Backend**: Node.js 20+, Express 4, TypeScript
- **Frontend**: JSP, jQuery 1.11.3/2.1.4, jqGrid 4.7.1 - **Frontend**: Next.js (App Router, Turbopack), React, shadcn/ui, Tailwind CSS
- **Database**: PostgreSQL - **Database**: PostgreSQL (pg 드라이버 직접 사용)
- **WAS**: Apache Tomcat 7.0 - **인증**: JWT (jsonwebtoken)
- **Build**: Eclipse IDE 기반 (Maven/Gradle 미사용) - **빌드**: npm, TypeScript (tsc)
- **개발도구**: nodemon (백엔드 핫리로드), Turbopack (프론트엔드)
## 프로젝트 구조
```
ERP-node/
├── backend-node/ # Express + TypeScript 백엔드
│ ├── src/
│ │ ├── app.ts # 엔트리포인트
│ │ ├── controllers/ # API 컨트롤러
│ │ ├── services/ # 비즈니스 로직
│ │ ├── middleware/ # 인증, 에러처리 미들웨어
│ │ ├── routes/ # 라우터
│ │ └── config/ # DB 연결 등 설정
│ └── package.json
├── frontend/ # Next.js 프론트엔드
│ ├── app/ # App Router 페이지
│ ├── components/ # React 컴포넌트 (shadcn/ui)
│ ├── lib/ # 유틸리티, API 클라이언트
│ ├── hooks/ # Custom React Hooks
│ └── package.json
├── db/ # 데이터베이스 마이그레이션 SQL
│ └── migrations/ # 순차 마이그레이션 파일
├── docker/ # Docker 설정 (dev/prod/deploy)
├── scripts/ # 개발/배포 스크립트
└── docs/ # 프로젝트 문서
```
## 주요 기능 ## 주요 기능
- 제품 정보 관리 - 사용자 및 권한 관리 (멀티테넌시)
- BOM (Bill of Materials) 관리 - 메뉴 및 화면 관리
- 설계 변경 관리 (ECO/ECR) - 플로우(워크플로우) 관리
- 문서 관리 및 버전 제어 - BOM 관리
- 프로젝트/일정 관리 - 문서/파일 관리
- 사용자 및 권한 관리 - 화면 디자이너 (동적 화면 생성)
- 워크플로우 관리 - 메일 연동
- 외부 DB 연결
## 개발 환경
```bash
# 백엔드 (nodemon으로 자동 재시작)
cd backend-node && npm run dev
# 프론트엔드 (Turbopack)
cd frontend && npm run dev
```
## 주요 설정 파일 ## 주요 설정 파일
- [web.xml](mdc:WebContent/WEB-INF/web.xml) - 웹 애플리케이션 배포 설정 - `backend-node/.env` - 백엔드 환경 변수 (DB, JWT 등)
- [dispatcher-servlet.xml](mdc:WebContent/WEB-INF/dispatcher-servlet.xml) - Spring MVC 설정 - `frontend/.env.local` - 프론트엔드 환경 변수
- [docker-compose.dev.yml](mdc:docker-compose.dev.yml) - 개발환경 Docker 설정 - `docker/` - Docker Compose 설정 (dev/prod)
- [docker-compose.prod.yml](mdc:docker-compose.prod.yml) - 운영환경 Docker 설정 - `Dockerfile` - 프로덕션 멀티스테이지 빌드
- `Jenkinsfile` - CI/CD 파이프라인

View File

@ -1,248 +1,126 @@
--- ---
description: description:
globs: globs:
alwaysApply: true alwaysApply: true
--- ---
# 보안 가이드 # 보안 가이드
## 인증 및 인가 ## 인증 및 인가
### 세션 기반 인증 ### JWT 기반 인증
현재 시스템은 세션 기반 인증을 사용합니다: 현재 시스템은 JWT 토큰 기반 인증을 사용합니다:
```java ```typescript
// 사용자 인증 정보 저장 // 토큰 생성 (로그인 시)
PersonBean person = new PersonBean(); const token = jwt.sign(
person.setUserId(userId); { userId, companyCode, role },
person.setUserName(userName); process.env.JWT_SECRET,
request.getSession().setAttribute(Constants.PERSON_BEAN, person); { expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
);
// 인증 정보 조회 // 토큰 검증 (미들웨어)
PersonBean person = (PersonBean)request.getSession().getAttribute(Constants.PERSON_BEAN); const decoded = jwt.verify(token, process.env.JWT_SECRET);
if (person == null) { req.user = decoded;
// 로그인 페이지로 리다이렉트
}
``` ```
### 권한 관리 구조 ### 권한 관리 구조
- **AUTH_GROUP**: 권한 그룹 정의 - **auth_group**: 권한 그룹 정의
- **MENU_AUTH_GROUP**: 메뉴별 권한 그룹 매핑 - **menu_auth_group**: 메뉴별 권한 그룹 매핑
- **USER_AUTH**: 사용자별 권한 할당 - **user_auth**: 사용자별 권한 할당
### 메뉴 접근 권한 체크 ### 인증 미들웨어
```java `backend-node/src/middleware/authMiddleware.ts` 참조
public HashMap<String, Object> checkUserMenuAuth(HttpServletRequest request, Map<String, Object> paramMap) {
PersonBean person = (PersonBean)request.getSession().getAttribute(Constants.PERSON_BEAN);
String userId = person.getUserId();
paramMap.put("userId", userId);
paramMap.put("menuUrl", request.getRequestURI());
// 권한 체크 쿼리 실행
return sqlSession.selectOne("admin.checkUserMenuAuth", paramMap);
}
```
## 데이터 보안 ## 데이터 보안
### SQL 인젝션 방지 ### SQL 인젝션 방지
**올바른 방법** - 파라미터 바인딩 사용: **올바른 방법** - 파라미터 바인딩 사용:
```xml ```typescript
<select id="selectUser" parameterType="map" resultType="map"> const result = await pool.query(
SELECT * FROM user_info 'SELECT * FROM user_info WHERE user_id = $1',
WHERE user_id = #{userId} <!-- 안전한 파라미터 바인딩 --> [userId]
</select> );
``` ```
**위험한 방법** - 직접 문자열 치환: **위험한 방법** - 직접 문자열 삽입:
```xml ```typescript
<select id="selectUser" parameterType="map" resultType="map"> const result = await pool.query(
SELECT * FROM user_info `SELECT * FROM user_info WHERE user_id = '${userId}'`
WHERE user_id = '${userId}' <!-- SQL 인젝션 위험 --> );
</select>
``` ```
### 패스워드 보안 ### 패스워드 보안
```java ```typescript
// 패스워드 암호화 (EncryptUtil 사용) import bcrypt from 'bcryptjs';
String encryptedPassword = EncryptUtil.encrypt(plainPassword);
// 패스워드 검증 // 해싱
boolean isValid = EncryptUtil.matches(plainPassword, encryptedPassword); const hashedPassword = await bcrypt.hash(plainPassword, 10);
// 검증
const isValid = await bcrypt.compare(plainPassword, hashedPassword);
``` ```
### 입력값 검증 ### 입력값 검증
```java ```typescript
// CommonUtils를 사용한 입력값 정제 import Joi from 'joi';
String safeInput = CommonUtils.checkNull(request.getParameter("input"));
if (StringUtils.isEmpty(safeInput)) { const schema = Joi.object({
throw new IllegalArgumentException("필수 입력값이 누락되었습니다."); userId: Joi.string().required().max(50),
email: Joi.string().email().required(),
});
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).json({ success: false, message: error.message });
} }
``` ```
## 세션 보안 ## 보안 미들웨어
### 세션 설정 ### Helmet (보안 헤더)
[web.xml](mdc:WebContent/WEB-INF/web.xml)에서 세션 타임아웃 설정: ```typescript
```xml import helmet from 'helmet';
<session-config> app.use(helmet());
<session-timeout>1440</session-timeout> <!-- 24시간 -->
</session-config>
``` ```
### 세션 무효화 ### Rate Limiting
```java ```typescript
// 로그아웃 시 세션 무효화 import rateLimit from 'express-rate-limit';
@RequestMapping("/logout.do") const limiter = rateLimit({
public String logout(HttpServletRequest request) { windowMs: 15 * 60 * 1000,
HttpSession session = request.getSession(false); max: 100,
if (session != null) { });
session.invalidate(); app.use('/api/', limiter);
}
return "redirect:/login.do";
}
``` ```
## 파일 업로드 보안 ### CORS 설정
```typescript
### 파일 확장자 검증 import cors from 'cors';
```java app.use(cors({
public boolean isAllowedFileType(String fileName) { origin: process.env.CORS_ORIGIN,
String[] allowedExtensions = {".jpg", ".jpeg", ".png", ".gif", ".pdf", ".doc", ".docx", ".xls", ".xlsx"}; credentials: true,
String extension = fileName.toLowerCase().substring(fileName.lastIndexOf(".")); }));
return Arrays.asList(allowedExtensions).contains(extension);
}
```
### 파일 크기 제한
```java
// web.xml에서 파일 업로드 크기 제한
<multipart-config>
<max-file-size>10485760</max-file-size> <!-- 10MB -->
<max-request-size>52428800</max-request-size> <!-- 50MB -->
</multipart-config>
```
### 안전한 파일명 생성
```java
// FileRenameClass 사용하여 안전한 파일명 생성
String safeFileName = FileRenameClass.rename(originalFileName);
```
## XSS 방지
### 출력값 이스케이프
JSP에서 사용자 입력값 출력 시:
```jsp
<!-- 안전한 출력 -->
<c:out value="${userInput}" escapeXml="true"/>
<!-- 위험한 출력 -->
${userInput} <!-- XSS 공격 가능 -->
```
### JavaScript에서 데이터 처리
```javascript
// 안전한 방법
var safeData = $('<div>').text(userInput).html();
// 위험한 방법
var dangerousData = userInput; // XSS 공격 가능
```
## CSRF 방지
### 토큰 기반 CSRF 방지
```jsp
<!-- 폼에 CSRF 토큰 포함 -->
<form method="post" action="save.do">
<input type="hidden" name="csrfToken" value="${csrfToken}"/>
<!-- 기타 입력 필드 -->
</form>
```
```java
// 컨트롤러에서 CSRF 토큰 검증
@RequestMapping("/save.do")
public String save(HttpServletRequest request, @RequestParam Map<String, Object> paramMap) {
String sessionToken = (String)request.getSession().getAttribute("csrfToken");
String requestToken = (String)paramMap.get("csrfToken");
if (!sessionToken.equals(requestToken)) {
throw new SecurityException("CSRF 토큰이 일치하지 않습니다.");
}
// 정상 처리
}
```
## 로깅 및 감사
### 보안 이벤트 로깅
```java
private static final Logger securityLogger = LoggerFactory.getLogger("SECURITY");
public void logSecurityEvent(String event, String userId, String details) {
securityLogger.info("Security Event: {} | User: {} | Details: {}", event, userId, details);
}
// 사용 예시
logSecurityEvent("LOGIN_SUCCESS", userId, request.getRemoteAddr());
logSecurityEvent("ACCESS_DENIED", userId, request.getRequestURI());
```
### 민감 정보 마스킹
```java
public String maskSensitiveData(String data) {
if (data == null || data.length() < 4) {
return "****";
}
return data.substring(0, 2) + "****" + data.substring(data.length() - 2);
}
```
## 환경별 보안 설정
### 개발 환경
- 상세한 오류 메시지 표시
- 디버그 모드 활성화
- 보안 제약 완화
### 운영 환경
- 일반적인 오류 메시지만 표시
- 디버그 모드 비활성화
- 엄격한 보안 정책 적용
```java
// 환경별 설정 예시
if (Constants.IS_PRODUCTION) {
// 운영 환경 설정
response.sendError(HttpServletResponse.SC_FORBIDDEN, "접근이 거부되었습니다.");
} else {
// 개발 환경 설정
response.sendError(HttpServletResponse.SC_FORBIDDEN, "권한 부족: " + detailedMessage);
}
``` ```
## 보안 체크리스트 ## 보안 체크리스트
### 코드 레벨 ### 코드 레벨
- [ ] SQL 인젝션 방지 (#{} 파라미터 바인딩 사용) - [ ] SQL 인젝션 방지 ($1 파라미터 바인딩 사용)
- [ ] XSS 방지 (출력값 이스케이프) - [ ] XSS 방지 (React 자동 이스케이프 활용)
- [ ] CSRF 방지 (토큰 검증) - [ ] 입력값 검증 (Joi 스키마)
- [ ] 입력값 검증 및 정제 - [ ] 패스워드 bcrypt 해싱
- [ ] 패스워드 암호화 저장 - [ ] JWT 토큰 만료 설정
### 설정 레벨 ### 설정 레벨
- [ ] 세션 타임아웃 설정 - [ ] Helmet 보안 헤더
- [ ] 파일 업로드 제한 - [ ] Rate Limiting 적용
- [ ] 오류 페이지 설정 - [ ] CORS 적절한 설정
- [ ] HTTPS 사용 (운영 환경) - [ ] HTTPS 사용 (운영 환경)
- [ ] 보안 헤더 설정 - [ ] 환경 변수로 시크릿 관리
### 운영 레벨 ### 운영 레벨
- [ ] 정기적인 보안 점검 - [ ] winston 로깅 모니터링
- [ ] 로그 모니터링
- [ ] 권한 정기 검토 - [ ] 권한 정기 검토
- [ ] 패스워드 정책 적용 - [ ] 패스워드 정책 적용
- [ ] 백업 데이터 암호화 - [ ] 백업 데이터 암호화

View File

@ -1,166 +0,0 @@
---
description:
globs:
alwaysApply: true
---
# 트러블슈팅 가이드
## 일반적인 문제 해결
### 애플리케이션 시작 오류
#### 데이터베이스 연결 실패
```
원인: JNDI DataSource 설정 문제
해결:
1. tomcat-conf/context.xml 확인
2. PostgreSQL 서비스 상태 확인
3. 데이터베이스 접속 정보 확인
```
#### 클래스 로딩 오류
```
원인: 컴파일되지 않은 Java 파일
해결:
1. Eclipse에서 프로젝트 Clean & Build
2. WebContent/WEB-INF/classes/ 디렉토리 확인
3. 누락된 라이브러리 확인 (WebContent/WEB-INF/lib/)
```
### 런타임 오류
#### 404 오류 (페이지를 찾을 수 없음)
```
원인: URL 매핑 문제
해결:
1. @RequestMapping 어노테이션 확인
2. JSP 파일 경로 확인 (/WEB-INF/view/)
3. web.xml의 servlet-mapping 확인 (*.do 패턴)
```
#### 500 오류 (서버 내부 오류)
```
원인: Java 코드 실행 오류
해결:
1. 로그 파일 확인 (log4j 설정)
2. MyBatis SQL 오류 확인
3. NullPointerException 체크
4. 데이터베이스 트랜잭션 오류 확인
```
### 데이터베이스 관련 문제
#### SQL 실행 오류
```
원인: MyBatis 매퍼 설정 문제
해결:
1. mapper XML 파일의 SQL 문법 확인
2. parameterType과 resultType 확인
3. 테이블/컬럼명 대소문자 확인 (PostgreSQL)
```
#### 트랜잭션 문제
```
원인: SqlSession 관리 문제
해결:
1. SqlSession.close() 호출 확인
2. try-finally 블록에서 리소스 정리
3. 트랜잭션 커밋/롤백 처리
```
### 프론트엔드 문제
#### JavaScript 오류
```
원인: jQuery/jqGrid 라이브러리 문제
해결:
1. 브라우저 개발자 도구 콘솔 확인
2. JavaScript 파일 로딩 순서 확인
3. jQuery 버전 호환성 확인
```
#### CSS 스타일 문제
```
원인: CSS 파일 로딩 또는 경로 문제
해결:
1. CSS 파일 경로 확인
2. 브라우저 캐시 클리어
3. all.css 파일 확인
```
## 개발 환경 문제
### Docker 환경
```bash
# 컨테이너 로그 확인
docker-compose logs app
# 컨테이너 내부 접속
docker-compose exec app bash
# 데이터베이스 접속 확인
docker-compose exec db psql -U postgres -d ilshin
```
### Eclipse 환경
```
문제: 프로젝트 인식 오류
해결:
1. .project 파일 확인
2. .classpath 파일 확인
3. Project Properties > Java Build Path 확인
4. Server Runtime 설정 확인 (Tomcat 7.0)
```
## 로그 분석
### 주요 로그 위치
- 애플리케이션 로그: log4j 설정에 따름
- Tomcat 로그: `$CATALINA_HOME/logs/`
- Docker 로그: `docker-compose logs`
### 로그 설정 파일
- [log4j.xml](mdc:WebContent/WEB-INF/log4j.xml) - 로깅 설정
- 로그 레벨 조정으로 상세 정보 확인 가능
## 성능 문제
### 데이터베이스 성능
```sql
-- 슬로우 쿼리 확인
SELECT query, mean_time, calls
FROM pg_stat_statements
ORDER BY mean_time DESC;
-- 인덱스 사용률 확인
SELECT schemaname, tablename, indexname, idx_scan, idx_tup_read, idx_tup_fetch
FROM pg_stat_user_indexes;
```
### 메모리 문제
```
원인: 메모리 누수 또는 부족
해결:
1. JVM 힙 메모리 설정 확인
2. SqlSession 리소스 정리 확인
3. 대량 데이터 처리 시 페이징 적용
```
## 보안 관련
### 권한 문제
```
원인: 사용자 권한 설정 오류
해결:
1. MENU_AUTH_GROUP 테이블 확인
2. 사용자 세션 정보 확인
3. Spring Security 설정 확인 (있는 경우)
```
### SQL 인젝션 방지
```
주의사항:
1. MyBatis #{} 파라미터 바인딩 사용
2. ${} 직접 문자열 치환 지양
3. 사용자 입력값 검증
```

View File

@ -1,26 +0,0 @@
# PLM ILSHIN 개발환경 설정
# 애플리케이션 환경
NODE_ENV=development
# 데이터베이스 설정
DB_URL=jdbc:postgresql://39.117.244.52:11132/plm
DB_USERNAME=postgres
DB_PASSWORD=ph0909!!
# PostgreSQL 환경 변수 (내부 DB 사용 시)
POSTGRES_DB=plm
POSTGRES_USER=postgres
POSTGRES_PASSWORD=ph0909!!
# 애플리케이션 포트
APP_PORT=8090
# JVM 옵션
JAVA_OPTS="-Xms512m -Xmx1024m -XX:PermSize=256m -XX:MaxPermSize=512m"
# 로그 레벨
LOG_LEVEL=DEBUG
# 개발 모드 플래그
DEBUG=true

328
.gitignore vendored
View File

@ -1,5 +1,12 @@
# Claude Code
.claude/
# Dependencies # Dependencies
node_modules/ node_modules/
jspm_packages/
bower_components
# Package manager logs
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
@ -10,138 +17,125 @@ pids
*.seed *.seed
*.pid.lock *.pid.lock
# Coverage directory used by tools like istanbul # Coverage
coverage/ coverage/
*.lcov *.lcov
# nyc test coverage
.nyc_output .nyc_output
lib-cov
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
jspm_packages/
# TypeScript cache # TypeScript cache
*.tsbuildinfo *.tsbuildinfo
# Optional npm cache directory # Build outputs
dist/
build/
build/Release
# Cache
.npm .npm
# Optional eslint cache
.eslintcache .eslintcache
.cache/
# Microbundle cache .parcel-cache
.rpt2_cache/ .rpt2_cache/
.rts2_cache_cjs/ .rts2_cache_cjs/
.rts2_cache_es/ .rts2_cache_es/
.rts2_cache_umd/ .rts2_cache_umd/
*.cache
cache/
# Optional REPL history # Next.js
.next
# Storybook
.out
.storybook-out
# REPL history
.node_repl_history .node_repl_history
# Output of 'npm pack' # Package archives
*.tgz *.tgz
# Yarn Integrity file
.yarn-integrity .yarn-integrity
# dotenv environment variables file # Temporary
tmp/
temp/
*.tmp
*.temp
# Logs
logs/
log/
*.log
# ===== 환경 변수 및 민감 정보 =====
# 환경 변수
.env .env
.env.test .env.test
.env.local .env.local
.env.production .env.production
.env.docker
backend-node/.env
backend-node/.env.local
backend-node/.env.development
backend-node/.env.production
backend-node/.env.test
frontend/.env
frontend/.env.local
frontend/.env.development
frontend/.env.production
frontend/.env.test
# parcel-bundler cache (https://parceljs.org/) # Docker
.cache docker-compose.override.yml
.parcel-cache docker-compose.prod.yml
# Next.js build output # 설정 파일
.next configs/
settings/
# Nuxt.js build / generate output # 키/인증서
.nuxt *.key
dist *.pem
*.p12
*.pfx
*.crt
*.cert
secrets/
secrets.json
secrets.yaml
secrets.yml
api-keys.json
tokens.json
# Build cache # 데이터베이스 덤프
.cache/ *.sql
*.dump
db/dump/
db/backup/
# Storybook build outputs # 백업
.out *.bak
.storybook-out *.backup
*.old
backup/
# Temporary folders # ===== IDE =====
tmp/
temp/
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# IDE files
.vscode/ .vscode/
.idea/ .idea/
*.iml
*.ipr
*.iws
.project
.classpath
.settings/
*.swp *.swp
*.swo *.swo
*~ *~
*.user
# OS generated files # OS
.DS_Store .DS_Store
.DS_Store? .DS_Store?
._* ._*
@ -150,127 +144,7 @@ jspm_packages/
ehthumbs.db ehthumbs.db
Thumbs.db Thumbs.db
# Build outputs # ===== 업로드/미디어 =====
dist/
build/
# Test coverage
coverage/
# ===== 민감한 정보 보호 =====
# 데이터베이스 연결 정보
backend-node/.env
backend-node/.env.local
backend-node/.env.development
backend-node/.env.production
backend-node/.env.test
# 백엔드 환경 변수
backend/.env
backend/.env.local
backend/.env.development
backend/.env.production
backend/.env.test
# 프론트엔드 환경 변수
frontend/.env
frontend/.env.local
frontend/.env.development
frontend/.env.production
frontend/.env.test
# Docker 관련 민감 정보
docker-compose.override.yml
docker-compose.prod.yml
.env.docker
# 설정 파일들
configs/
settings/
*.config.js
*.config.ts
*.config.json
# 로그 파일들
*.log
logs/
log/
# 임시 파일들
*.tmp
*.temp
temp/
tmp/
# 백업 파일들
*.bak
*.backup
*.old
backup/
# 키 파일들
*.key
*.pem
*.p12
*.pfx
*.crt
*.cert
# API 키 및 토큰
secrets/
secrets.json
secrets.yaml
secrets.yml
api-keys.json
tokens.json
# 데이터베이스 덤프 파일
*.sql
*.dump
*.backup
db/dump/
db/backup/
# 캐시 파일들
.cache/
cache/
*.cache
# 사용자별 설정
.vscode/settings.json
.idea/workspace.xml
*.user
# ===== Gradle 관련 파일들 (레거시 Java 프로젝트) =====
# Gradle 캐시 및 빌드 파일들
.gradle/
*/.gradle/
gradle/
gradlew
gradlew.bat
gradle.properties
build/
*/build/
# Gradle Wrapper
gradle-wrapper.jar
gradle-wrapper.properties
# IntelliJ IDEA 관련 (Gradle 프로젝트)
.idea/
*.iml
*.ipr
*.iws
out/
# Eclipse 관련 (Gradle 프로젝트)
.project
.classpath
.settings/
bin/
# 업로드된 파일들 제외
backend-node/uploads/ backend-node/uploads/
uploads/ uploads/
*.jpg *.jpg
@ -286,4 +160,20 @@ uploads/
*.hwp *.hwp
*.hwpx *.hwpx
claude.md # ===== 기타 =====
claude.md
# Agent Pipeline 로컬 파일
_local/
.agent-pipeline/
.codeguard-baseline.json
scripts/browser-test-*.js
# AI 에이전트 테스트 산출물
*-test-screenshots/
*-screenshots/
*-test.mjs
# 개인 작업 문서
popdocs/
.cursor/rules/popdocs-safety.mdc

View File

@ -1,47 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ilshin</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.wst.jsdt.core.javascriptValidator</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.common.project.facet.core.builder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.validation.validationbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jem.workbench.JavaEMFNature</nature>
<nature>org.eclipse.wst.common.modulecore.ModuleCoreNature</nature>
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.wst.jsdt.core.jsNature</nature>
</natures>
<filteredResources>
<filter>
<id>1746619144814</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry excluding="**/*.min.js|**/node_modules/*|**/bower_components/*" kind="src" path="WebContent"/>
<classpathentry kind="con" path="org.eclipse.wst.jsdt.launching.JRE_CONTAINER"/>
<classpathentry kind="con" path="org.eclipse.wst.jsdt.launching.WebProject">
<attributes>
<attribute name="hide" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.wst.jsdt.launching.baseBrowserLibrary"/>
<classpathentry kind="output" path=""/>
</classpath>

View File

@ -1,2 +0,0 @@
eclipse.preferences.version=1
encoding//WebContent/WEB-INF/view/materMgmt/materOrderDown1.jsp=UTF-8

View File

@ -1,12 +0,0 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.7
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=1.7

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><project-modules id="moduleCoreId" project-version="1.5.0">
<wb-module deploy-name="plm">
<wb-resource deploy-path="/" source-path="/WebContent" tag="defaultRootSource"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src"/>
<property name="context-root" value="plm"/>
<property name="java-output-path" value="/plm/build/classes"/>
</wb-module>
</project-modules>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<faceted-project>
<runtime name="ilshin"/>
<fixed facet="wst.jsdt.web"/>
<fixed facet="jst.web"/>
<fixed facet="java"/>
<installed facet="java" version="1.7"/>
<installed facet="jst.web" version="3.0"/>
<installed facet="wst.jsdt.web" version="1.0"/>
</faceted-project>

View File

@ -1 +0,0 @@
org.eclipse.wst.jsdt.launching.baseBrowserLibrary

View File

@ -1 +0,0 @@
Window

View File

@ -1,8 +0,0 @@
DELEGATES_PREFERENCE=delegateValidatorList
USER_BUILD_PREFERENCE=enabledBuildValidatorListorg.eclipse.jst.j2ee.internal.classpathdep.ClasspathDependencyValidator;
USER_MANUAL_PREFERENCE=enabledManualValidatorListorg.eclipse.jst.j2ee.internal.classpathdep.ClasspathDependencyValidator;
USER_PREFERENCE=overrideGlobalPreferencestruedisableAllValidationtrueversion1.2.700.v201508251749
eclipse.preferences.version=1
override=true
suspend=true
vf.version=3

436
PLAN.MD
View File

@ -1,139 +1,337 @@
# 프로젝트: V2/V2 컴포넌트 설정 스키마 정비 # 현재 구현 계획: pop-card-list 입력 필드/계산 필드 구조 개편
## 개요 > **작성일**: 2026-02-24
> **상태**: 계획 완료, 코딩 대기
레거시 컴포넌트를 제거하고, V2/V2 컴포넌트 전용 Zod 스키마와 기본값 레지스트리를 한 곳에서 관리한다. > **목적**: 입력 필드 설정 단순화 + 본문 필드에 계산식 통합 + 기존 계산 필드 섹션 제거
## 핵심 기능
1. [x] 레거시 컴포넌트 스키마 제거
2. [x] V2 컴포넌트 overrides 스키마 정의 (16개)
3. [x] V2 컴포넌트 overrides 스키마 정의 (9개)
4. [x] componentConfig.ts 한 파일에서 통합 관리
## 정의된 V2 컴포넌트 (18개)
- v2-table-list, v2-button-primary, v2-text-display
- v2-split-panel-layout, v2-section-card, v2-section-paper
- v2-divider-line, v2-repeat-container, v2-rack-structure
- v2-numbering-rule, v2-category-manager, v2-pivot-grid
- v2-location-swap-selector, v2-aggregation-widget
- v2-card-display, v2-table-search-widget, v2-tabs-widget
- v2-v2-repeater
## 정의된 V2 컴포넌트 (9개)
- v2-input, v2-select, v2-date
- v2-list, v2-layout, v2-group
- v2-media, v2-biz, v2-hierarchy
## 테스트 계획
### 1단계: 기본 기능
- [x] V2 레이아웃 저장 시 컴포넌트별 overrides 스키마 검증 통과
- [x] V2 컴포넌트 기본값과 스키마가 매칭됨
### 2단계: 에러 케이스
- [x] 잘못된 overrides 입력 시 Zod 검증 실패 처리 (safeParse + console.warn + graceful fallback)
- [x] 누락된 기본값 컴포넌트 저장 시 안전한 기본값 적용 (레지스트리 조회 → 빈 객체)
## 에러 처리 계획
- 스키마 파싱 실패 시 로그/에러 메시지 표준화
- 기본값 누락 시 안전한 fallback 적용
## 진행 상태
- [x] 레거시 컴포넌트 제거 완료
- [x] V2/V2 스키마 정의 완료
- [x] 한 파일 통합 관리 완료
# 프로젝트: 화면 복제 기능 개선 (DB 구조 개편 후)
## 개요
채번/카테고리에서 `menu_objid` 의존성 제거 완료 후, 화면 복제 기능을 새 DB 구조에 맞게 수정하고 테스트합니다.
## 핵심 변경사항
### DB 구조 변경 (완료)
- 채번규칙: `menu_objid` 의존성 제거 → `table_name + column_name + company_code` 기반
- 카테고리: `menu_objid` 의존성 제거 → `table_name + column_name + company_code` 기반
- 복제 순서 의존성 문제 해결
### 복제 옵션 정리 (완료)
- [x] **삭제**: 코드 카테고리 + 코드 복사 옵션
- [x] **삭제**: 연쇄관계 설정 복사 옵션
- [x] **이름 변경**: "카테고리 매핑 + 값 복사" → "카테고리 값 복사"
### 현재 복제 옵션 (3개)
1. **채번 규칙 복사** - 채번규칙 복제
2. **카테고리 값 복사** - 카테고리 값 복제 (table_column_category_values)
3. **테이블 타입관리 입력타입 설정 복사** - table_type_columns 복제
--- ---
## 테스트 계획 ## 1. 변경 개요
### 1. 화면 간 연결 복제 테스트 ### 배경
- 기존: "입력 필드", "계산 필드", "담기 버튼" 3개가 별도 섹션으로 분리
- 문제: 계산 필드가 본문 필드와 동일한 위치에 표시되어야 하는데 별도 영역에 있음
- 문제: 입력 필드의 min/max 고정값은 비실용적 (실제로는 DB 컬럼 기준 제한이 필요)
- 문제: step, columnName, sourceColumns, resultColumn 등 죽은 코드 존재
- [ ] 수주관리 1번→2번→3번→4번 화면 연결 상태에서 복제 ### 목표
- [ ] 복제 후 연결 관계가 유지되는지 확인 1. **본문 필드에 계산식 지원 추가** - 필드별로 "DB 컬럼" 또는 "계산식" 선택
- [ ] 각 화면의 고유 키값이 새로운 화면을 참조하도록 변경되는지 확인 2. **입력 필드 설정 단순화** - 고정 min/max 제거, 제한 기준 컬럼 방식으로 변경
3. **기존 "계산 필드" 섹션 제거** - 본문 필드에 통합되므로 불필요
### 2. 제어관리 복제 테스트 4. **죽은 코드 정리**
- [ ] 다른 회사로 제어관리 복제
- [ ] 복제된 플로우 스텝/연결이 정상 작동하는지 확인
### 3. 추가 옵션 복제 테스트
- [ ] 채번규칙 복사 정상 작동 확인
- [ ] 카테고리 값 복사 정상 작동 확인
- [ ] 테이블 타입관리 입력타입 설정 복사 정상 작동 확인
### 4. 기본 복제 테스트
- [ ] 단일 화면 복제 (모달 포함)
- [ ] 그룹 전체 복제 (재귀적)
- [ ] 메뉴 동기화 정상 작동
--- ---
## 관련 파일 ## 2. 수정 대상 파일 (3개)
- `frontend/components/screen/CopyScreenModal.tsx` - 복제 모달 ### 파일 A: `frontend/lib/registry/pop-components/types.ts`
- `frontend/components/screen/ScreenGroupTreeView.tsx` - 트리 뷰 + 컨텍스트 메뉴
- `backend-node/src/services/screenManagementService.ts` - 복제 서비스
- `backend-node/src/services/numberingRuleService.ts` - 채번규칙 서비스
- `docs/DB_STRUCTURE_DIAGRAM.md` - DB 구조 문서
## 진행 상태 #### 변경 A-1: CardFieldBinding 타입 확장
- [완료] DB 구조 개편 (menu_objid 의존성 제거) **현재 코드** (라인 367~372):
- [완료] 복제 옵션 정리 (코드카테고리/연쇄관계 삭제, 이름 변경) ```typescript
- [완료] 화면 간 연결 복제 버그 수정 (targetScreenId 매핑 추가) export interface CardFieldBinding {
- [대기] 화면 간 연결 복제 테스트 id: string;
- [대기] 제어관리 복제 테스트 columnName: string;
- [대기] 추가 옵션 복제 테스트 label: string;
textColor?: string;
}
```
**변경 코드**:
```typescript
export interface CardFieldBinding {
id: string;
label: string;
textColor?: string;
valueType: "column" | "formula"; // 값 유형: DB 컬럼 또는 계산식
columnName?: string; // valueType === "column"일 때 사용
formula?: string; // valueType === "formula"일 때 사용 (예: "$input - received_qty")
unit?: string; // 계산식일 때 단위 표시 (예: "EA")
}
```
**주의**: `columnName`이 required에서 optional로 변경됨. 기존 저장 데이터와의 하위 호환 필요.
#### 변경 A-2: CardInputFieldConfig 단순화
**현재 코드** (라인 443~453):
```typescript
export interface CardInputFieldConfig {
enabled: boolean;
columnName?: string;
label?: string;
unit?: string;
defaultValue?: number;
min?: number;
max?: number;
maxColumn?: string;
step?: number;
}
```
**변경 코드**:
```typescript
export interface CardInputFieldConfig {
enabled: boolean;
label?: string;
unit?: string;
limitColumn?: string; // 제한 기준 컬럼 (해당 행의 이 컬럼 값이 최대값)
saveTable?: string; // 저장 대상 테이블
saveColumn?: string; // 저장 대상 컬럼
showPackageUnit?: boolean; // 포장등록 버튼 표시 여부
}
```
**제거 항목**:
- `columnName` -> `saveTable` + `saveColumn`으로 대체 (명확한 네이밍)
- `defaultValue` -> 제거 (제한 기준 컬럼 값으로 대체)
- `min` -> 제거 (항상 0)
- `max` -> 제거 (`limitColumn`으로 대체)
- `maxColumn` -> `limitColumn`으로 이름 변경
- `step` -> 제거 (키패드 방식에서 미사용)
#### 변경 A-3: CardCalculatedFieldConfig 제거
**삭제**: `CardCalculatedFieldConfig` 인터페이스 전체 (라인 457~464)
**삭제**: `PopCardListConfig`에서 `calculatedField?: CardCalculatedFieldConfig;` 제거
--- ---
## 수정 이력 ### 파일 B: `frontend/lib/registry/pop-components/pop-card-list/PopCardListConfig.tsx`
### 2026-01-26: 버튼 targetScreenId 매핑 버그 수정 #### 변경 B-1: 본문 필드 편집기(FieldEditor)에 값 유형 선택 추가
**문제**: 그룹 복제 시 버튼의 `targetScreenId`가 새 화면으로 매핑되지 않음 **현재**: 필드 편집 시 라벨, 컬럼, 텍스트색상만 설정 가능
- 수주관리 1→2→3→4 화면 복제 시 연결이 깨지는 문제 **변경**: 값 유형 라디오("DB 컬럼" / "계산식") 추가
- "DB 컬럼" 선택 시: 기존 컬럼 Select 표시
- "계산식" 선택 시: 수식 입력란 + 사용 가능한 컬럼/변수 칩 목록 표시
- 사용 가능한 변수: DB 컬럼명들 + `$input` (입력 필드 활성화 시)
**수정 파일**: `backend-node/src/services/screenManagementService.ts` **하위 호환**: 기존 저장 데이터에 `valueType`이 없으면 `"column"`으로 기본 처리
- `updateTabScreenReferences` 함수에 `targetScreenId` 처리 로직 추가 #### 변경 B-2: 입력 필드 설정 섹션 개편
- 쿼리에 `targetScreenId` 검색 조건 추가
- 문자열/숫자 타입 모두 처리 **현재 설정 항목**: 라벨, 단위, 기본값, 최소/최대, 최대값 컬럼, 저장 컬럼
**변경 설정 항목**:
```
라벨 [입고 수량 ]
단위 [EA ]
제한 기준 컬럼 [ order_qty v ]
저장 대상 테이블 [ 선택 v ]
저장 대상 컬럼 [ 선택 v ]
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
포장등록 버튼 [on/off]
```
#### 변경 B-3: "계산 필드" 섹션 제거
**삭제**: `CalculatedFieldSettingsSection` 함수 전체
**삭제**: 카드 템플릿 탭에서 "계산 필드" CollapsibleSection 제거
#### 변경 B-4: import 정리
**삭제**: `CardCalculatedFieldConfig` import
**추가**: 없음 (기존 import 재사용)
---
### 파일 C: `frontend/lib/registry/pop-components/pop-card-list/PopCardListComponent.tsx`
#### 변경 C-1: FieldRow에서 계산식 필드 지원
**현재**: `const value = row[field.columnName]` 로 DB 값만 표시
**변경**:
```typescript
function FieldRow({ field, row, scaled, inputValue }: {
field: CardFieldBinding;
row: RowData;
scaled: ScaledConfig;
inputValue?: number; // 입력 필드 값 (계산식에서 $input으로 참조)
}) {
const value = field.valueType === "formula" && field.formula
? evaluateFormula(field.formula, row, inputValue ?? 0)
: row[field.columnName ?? ""];
// ...
}
```
**주의**: `inputValue`를 FieldRow까지 전달해야 하므로 CardItem -> FieldRow 경로에 prop 추가 필요
#### 변경 C-2: 계산식 필드 실시간 갱신
**현재**: 별도 `calculatedValue` useMemo가 `[calculatedField, row, inputValue]`에 반응
**변경**: FieldRow가 `inputValue` prop을 받으므로, `inputValue`가 변경될 때 계산식 필드가 자동으로 리렌더링됨. 별도 useMemo 불필요.
#### 변경 C-3: 기존 calculatedField 관련 코드 제거
**삭제 대상**:
- `calculatedField` prop 전달 (CardItem)
- `calculatedValue` useMemo
- 계산 필드 렌더링 블록 (`{calculatedField?.enabled && calculatedValue !== null && (...)}`
#### 변경 C-4: 입력 필드 로직 단순화
**변경 대상**:
- `effectiveMax`: `limitColumn` 사용, 미설정 시 999999 폴백
- `defaultValue` 자동 초기화 로직 제거 (불필요)
- `NumberInputModal`에 포장등록 on/off 전달
#### 변경 C-5: NumberInputModal에 포장등록 on/off 전달
**현재**: 포장등록 버튼 항상 표시
**변경**: `showPackageUnit` prop 추가, false이면 포장등록 버튼 숨김
---
### 파일 D: `frontend/lib/registry/pop-components/pop-card-list/NumberInputModal.tsx`
#### 변경 D-1: showPackageUnit prop 추가
**현재 props**: open, onOpenChange, unit, initialValue, initialPackageUnit, min, maxValue, onConfirm
**추가 prop**: `showPackageUnit?: boolean` (기본값 true)
**변경**: `showPackageUnit === false`이면 포장등록 버튼 숨김
---
## 3. 구현 순서 (의존성 기반)
| 순서 | 작업 | 파일 | 의존성 | 상태 |
|------|------|------|--------|------|
| 1 | A-1: CardFieldBinding 타입 확장 | types.ts | 없음 | [ ] |
| 2 | A-2: CardInputFieldConfig 단순화 | types.ts | 없음 | [ ] |
| 3 | A-3: CardCalculatedFieldConfig 제거 | types.ts | 없음 | [ ] |
| 4 | B-1: FieldEditor에 값 유형 선택 추가 | PopCardListConfig.tsx | 순서 1 | [ ] |
| 5 | B-2: 입력 필드 설정 섹션 개편 | PopCardListConfig.tsx | 순서 2 | [ ] |
| 6 | B-3: 계산 필드 섹션 제거 | PopCardListConfig.tsx | 순서 3 | [ ] |
| 7 | B-4: import 정리 | PopCardListConfig.tsx | 순서 6 | [ ] |
| 8 | D-1: NumberInputModal showPackageUnit 추가 | NumberInputModal.tsx | 없음 | [ ] |
| 9 | C-1: FieldRow 계산식 지원 | PopCardListComponent.tsx | 순서 1 | [ ] |
| 10 | C-3: calculatedField 관련 코드 제거 | PopCardListComponent.tsx | 순서 9 | [ ] |
| 11 | C-4: 입력 필드 로직 단순화 | PopCardListComponent.tsx | 순서 2, 8 | [ ] |
| 12 | 린트 검사 | 전체 | 순서 1~11 | [ ] |
순서 1, 2, 3은 독립이므로 병렬 가능.
순서 8은 독립이므로 병렬 가능.
---
## 4. 사전 충돌 검사 결과
### 새로 추가할 식별자 목록
| 식별자 | 타입 | 정의 파일 | 사용 파일 | 충돌 여부 |
|--------|------|-----------|-----------|-----------|
| `valueType` | CardFieldBinding 속성 | types.ts | PopCardListConfig.tsx, PopCardListComponent.tsx | 충돌 없음 |
| `formula` | CardFieldBinding 속성 | types.ts | PopCardListConfig.tsx, PopCardListComponent.tsx | 충돌 없음 (기존 CardCalculatedFieldConfig.formula와 다른 인터페이스) |
| `limitColumn` | CardInputFieldConfig 속성 | types.ts | PopCardListConfig.tsx, PopCardListComponent.tsx | 충돌 없음 |
| `saveTable` | CardInputFieldConfig 속성 | types.ts | PopCardListConfig.tsx | 충돌 없음 |
| `saveColumn` | CardInputFieldConfig 속성 | types.ts | PopCardListConfig.tsx | 충돌 없음 |
| `showPackageUnit` | CardInputFieldConfig 속성 / NumberInputModal prop | types.ts, NumberInputModal.tsx | PopCardListComponent.tsx | 충돌 없음 |
### 기존 타입/함수 재사용 목록
| 기존 식별자 | 정의 위치 | 이번 수정에서 사용하는 곳 |
|------------|-----------|------------------------|
| `evaluateFormula()` | PopCardListComponent.tsx 라인 1026 | C-1: FieldRow에서 호출 (기존 함수 그대로 재사용) |
| `CardFieldBinding` | types.ts 라인 367 | A-1에서 수정, B-1/C-1에서 사용 |
| `CardInputFieldConfig` | types.ts 라인 443 | A-2에서 수정, B-2/C-4에서 사용 |
| `GroupedColumnSelect` | PopCardListConfig.tsx | B-1: 계산식 모드에서 컬럼 칩 표시에 재사용 가능 |
**사용처 있는데 정의 누락된 항목: 없음**
---
## 5. 에러 함정 경고
### 함정 1: 기존 저장 데이터 하위 호환
기존에 저장된 `CardFieldBinding`에는 `valueType`이 없고 `columnName`이 필수였음.
**반드시** 런타임에서 `field.valueType || "column"` 폴백 처리해야 함.
Config UI에서도 `valueType` 미존재 시 `"column"` 기본값 적용 필요.
### 함정 2: CardInputFieldConfig 하위 호환
기존 `maxColumn`이 `limitColumn`으로 이름 변경됨.
기존 저장 데이터의 `maxColumn`을 `limitColumn`으로 읽어야 함.
런타임: `inputField?.limitColumn || (inputField as any)?.maxColumn` 폴백 필요.
### 함정 3: evaluateFormula의 inputValue 전달
FieldRow에 `inputValue`를 전달하려면, CardItem -> body.fields.map -> FieldRow 경로에서 `inputValue` prop을 추가해야 함.
입력 필드가 비활성화된 경우 `inputValue`는 0으로 전달.
### 함정 4: calculatedField 제거 시 기존 데이터
기존 config에 `calculatedField` 데이터가 남아 있을 수 있음.
타입에서 제거하더라도 런타임 에러는 나지 않음 (unknown 속성은 무시됨).
다만 이전에 계산 필드로 설정한 내용은 사라짐 - 마이그레이션 없이 제거.
### 함정 5: columnName optional 변경
`CardFieldBinding.columnName`이 optional이 됨.
기존에 `row[field.columnName]`으로 직접 접근하던 코드 전부 수정 필요.
`field.columnName ?? ""` 또는 valueType 분기 처리.
---
## 6. 검증 방법
### 시나리오 1: 기존 본문 필드 (하위 호환)
1. 기존 저장된 카드리스트 열기
2. 본문 필드에 기존 DB 컬럼 필드가 정상 표시되는지 확인
3. 설정 패널에서 기존 필드가 "DB 컬럼" 유형으로 표시되는지 확인
### 시나리오 2: 계산식 본문 필드 추가
1. 본문 필드 추가 -> 값 유형 "계산식" 선택
2. 수식: `order_qty - received_qty` 입력
3. 카드에서 계산 결과가 정상 표시되는지 확인
### 시나리오 3: $input 참조 계산식
1. 입력 필드 활성화
2. 본문 필드 추가 -> 값 유형 "계산식" -> 수식: `$input - received_qty`
3. 키패드에서 수량 입력 시 계산 결과가 실시간 갱신되는지 확인
### 시나리오 4: 제한 기준 컬럼
1. 입력 필드 -> 제한 기준 컬럼: `order_qty`
2. order_qty=1000인 카드에서 키패드 열기
3. MAX 버튼 클릭 시 1000이 입력되고, 1001 이상 입력 불가 확인
### 시나리오 5: 포장등록 on/off
1. 입력 필드 -> 포장등록 버튼: off
2. 키패드 모달에서 포장등록 버튼이 숨겨지는지 확인
---
## 이전 완료 계획 (아카이브)
<details>
<summary>pop-dashboard 4가지 아이템 모드 완성 (완료)</summary>
- [x] groupBy UI 추가
- [x] xAxisColumn 입력 UI 추가
- [x] 통계카드 카테고리 설정 UI 추가
- [x] 차트 xAxisColumn 자동 보정 로직
- [x] 통계카드 카테고리별 필터 적용
- [x] SQL 빌더 방어 로직
- [x] refreshInterval 최소값 강제
</details>
<details>
<summary>POP 뷰어 스크롤 수정 (완료)</summary>
- [x] overflow-hidden 제거
- [x] overflow-auto 공통 적용
- [x] 일반 모드 min-h-full 추가
</details>
<details>
<summary>POP 뷰어 실제 컴포넌트 렌더링 (완료)</summary>
- [x] 뷰어 페이지에 레지스트리 초기화 import 추가
- [x] renderActualComponent() 실제 컴포넌트 렌더링으로 교체
</details>

696
POPUPDATE_2.md Normal file
View File

@ -0,0 +1,696 @@
# POP 컴포넌트 정의서 v8.0
## POP 헌법 (공통 규칙)
### 제1조. 컴포넌트의 정의
- 컴포넌트란 디자이너가 그리드에 배치하는 것이다
- 그리드에 배치하지 않는 것은 컴포넌트가 아니다
### 제2조. 컴포넌트의 독립성
- 모든 컴포넌트는 독립적으로 동작한다
- 컴포넌트는 다른 컴포넌트의 존재를 직접 알지 못한다 (이벤트 버스로만 통신)
### 제3조. 데이터의 자유
- 모든 컴포넌트는 자신의 테이블 + 외부 테이블을 자유롭게 조인할 수 있다
- 컬럼별로 read/write/readwrite/hidden을 개별 설정할 수 있다
- 보유 데이터 중 원하는 컬럼만 골라서 저장할 수 있다
### 제4조. 통신의 규칙
- 컴포넌트 간 통신은 반드시 이벤트 버스(usePopEvent)를 통한다
- 컴포넌트가 다른 컴포넌트를 직접 참조하거나 호출하지 않는다
- 이벤트는 화면 단위로 격리된다 (다른 POP 화면의 이벤트를 받지 않는다)
- 같은 화면 안에서는 이벤트를 통해 자유롭게 데이터를 주고받을 수 있다
### 제5조. 역할의 분리
- 조회용 입력(pop-search)과 저장용 입력(pop-field)은 다른 컴포넌트다
- 이동/실행(pop-icon)과 값 선택 후 반환(pop-lookup)은 다른 컴포넌트다
- 자주 쓰는 패턴은 하나로 합치되, 흐름 자체는 강제하고 보이는 방식만 옵션으로 제공한다
### 제6조. 시스템 설정도 컴포넌트다
- 프로필, 테마, 대시보드 보이기/숨기기 같은 시스템 설정도 컴포넌트(pop-system)로 만든다
- 디자이너가 pop-system을 배치하지 않으면 해당 화면에 설정 기능이 없다
- 이를 통해 디자이너가 "이 화면에 설정 기능을 넣을지 말지"를 직접 결정한다
### 제7조. 디자이너의 권한
- 디자이너는 컴포넌트를 배치하고 설정한다
- 디자이너는 사용자에게 커스텀을 허용할지 말지 결정한다 (userConfigurable)
- 디자이너가 "사용자 커스텀 허용 = OFF"로 설정하면, 사용자는 변경할 수 없다
- 컴포넌트의 옵션 설정(어떻게 저장하고 어떻게 조회하는지 등)은 디자이너가 결정한다
### 제8조. 컴포넌트의 구성
- 모든 컴포넌트는 3개 파일로 구성된다: 실제 컴포넌트, 디자인 미리보기, 설정 패널
- 모든 컴포넌트는 레지스트리에 등록해야 디자이너에 나타난다
- 모든 컴포넌트 인스턴스는 userConfigurable, displayName 공통 속성을 가진다
### 제9조. 모달 화면의 설계
- 모달은 인라인(컴포넌트 설정만으로 구성)과 외부 참조(별도 POP 화면 연결) 두 가지 방식이 있다
- 단순한 목록 선택은 인라인 모달을 사용한다 (설정만으로 완결)
- 복잡한 검색/필터가 필요하거나 여러 곳에서 재사용하는 모달은 별도 POP 화면을 만들어 참조한다
- 모달 안의 화면도 동일한 POP 컴포넌트 시스템으로 구성된다 (같은 그리드, 같은 컴포넌트)
- 모달 화면의 layout_data는 기존 screen_layouts_pop 테이블에 저장한다 (DB 변경 불필요)
---
## 현재 상태
- 그리드 시스템 (v5.2): 완성
- 컴포넌트 레지스트리: 완성 (PopComponentRegistry.ts)
- 구현 완료: `pop-text` 1개 (pop-text.tsx)
- 기존 `components-spec.md`는 v4 기준이라 갱신 필요
## 아키텍처 개요
```mermaid
graph TB
subgraph designer [디자이너]
Palette[컴포넌트 팔레트]
Grid[CSS Grid 캔버스]
ConfigPanel[속성 설정 패널]
end
subgraph registry [레지스트리]
Registry[PopComponentRegistry]
end
subgraph infra [공통 인프라]
DataSource[useDataSource 훅]
EventBus[usePopEvent 훅]
ActionRunner[usePopAction 훅]
end
subgraph components [9개 컴포넌트]
Text[pop-text - 완성]
Dashboard[pop-dashboard]
Table[pop-table]
Button[pop-button]
Icon[pop-icon]
Search[pop-search]
Field[pop-field]
Lookup[pop-lookup]
System[pop-system]
end
subgraph backend [기존 백엔드 API]
DataAPI[dataApi - 동적 CRUD]
DashAPI[dashboardApi - 통계 쿼리]
CodeAPI[commonCodeApi - 공통코드]
NumberAPI[numberingRuleApi - 채번]
end
Palette --> Grid
Grid --> ConfigPanel
ConfigPanel --> Registry
Registry --> components
components --> infra
infra --> backend
EventBus -.->|컴포넌트 간 통신| components
System -.->|보이기/숨기기 제어| components
```
---
## 공통 인프라 (모든 컴포넌트가 공유)
### 핵심 원칙: 모든 컴포넌트는 데이터를 자유롭게 다룬다
1. **데이터 전달**: 모든 컴포넌트는 자신이 보유한 데이터를 다른 컴포넌트에 전달/수신 가능
2. **테이블 조인**: 자신의 테이블 + 외부 테이블 자유롭게 조인하여 데이터 구성
3. **컬럼별 CRUD 제어**: 컬럼 단위로 "조회만" / "저장 대상" / "숨김"을 개별 설정 가능
4. **선택적 저장**: 보유 데이터 중 원하는 컬럼만 골라서 저장/수정/삭제 가능
### 공통 인스턴스 속성 (모든 컴포넌트 배치 시 설정 가능)
디자이너가 컴포넌트를 그리드에 배치할 때 설정하는 공통 속성:
- `userConfigurable`: boolean - 사용자가 이 컴포넌트를 숨길 수 있는지 (개인 설정 패널에 노출)
- `displayName`: string - 개인 설정 패널에 보여줄 이름 (예: "금일 생산실적")
### 1. DataSourceConfig (데이터 소스 설정 타입)
모든 데이터 연동 컴포넌트가 사용하는 표준 설정 구조:
- `tableName`: 대상 테이블
- `columns`: 컬럼 바인딩 목록 (ColumnBinding 배열)
- `filters`: 필터 조건 배열
- `sort`: 정렬 설정
- `aggregation`: 집계 함수 (count, sum, avg, min, max)
- `joins`: 테이블 조인 설정 (JoinConfig 배열)
- `refreshInterval`: 자동 새로고침 주기 (초)
- `limit`: 조회 건수 제한
### 1-1. ColumnBinding (컬럼별 읽기/쓰기 제어)
각 컬럼이 컴포넌트에서 어떤 역할을 하는지 개별 설정:
- `columnName`: 컬럼명
- `sourceTable`: 소속 테이블 (조인된 외부 테이블 포함)
- `mode`: "read" | "write" | "readwrite" | "hidden"
- read: 조회만 (화면에 표시하되 저장 안 함)
- write: 저장 대상 (사용자 입력 -> DB 저장)
- readwrite: 조회 + 저장 모두
- hidden: 내부 참조용 (화면에 안 보이지만 다른 컴포넌트에 전달 가능)
- `label`: 화면 표시 라벨
- `defaultValue`: 기본값
예시: 발주 품목 카드에서 5개 컬럼 중 3개만 저장
```
columns: [
{ columnName: "item_code", sourceTable: "order_items", mode: "read" },
{ columnName: "item_name", sourceTable: "item_info", mode: "read" },
{ columnName: "inbound_qty", sourceTable: "order_items", mode: "readwrite" },
{ columnName: "warehouse", sourceTable: "order_items", mode: "write" },
{ columnName: "memo", sourceTable: "order_items", mode: "write" },
]
```
### 1-2. JoinConfig (테이블 조인 설정)
외부 테이블과 자유롭게 조인:
- `targetTable`: 조인할 외부 테이블명
- `joinType`: "inner" | "left" | "right"
- `on`: 조인 조건 { sourceColumn, targetColumn }
- `columns`: 가져올 컬럼 목록
### 2. useDataSource 훅
DataSourceConfig를 받아서 기존 API를 호출하고 결과를 반환:
- 로딩/에러/데이터 상태 관리
- 자동 새로고침 타이머
- 필터 변경 시 자동 재조회
- 기존 `dataApi`, `dashboardApi` 활용
- **CRUD 함수 제공**: save(data), update(id, data), delete(id)
- ColumnBinding의 mode가 "write" 또는 "readwrite"인 컬럼만 저장 대상에 포함
- "read" 컬럼은 저장 시 자동 제외
### 3. usePopEvent 훅 (이벤트 버스 - 데이터 전달 포함)
컴포넌트 간 통신 (단순 이벤트 + 데이터 페이로드):
- `publish(eventName, payload)`: 이벤트 발행
- `subscribe(eventName, callback)`: 이벤트 구독
- `getSharedData(key)`: 공유 데이터 직접 읽기
- `setSharedData(key, value)`: 공유 데이터 직접 쓰기
- 화면 단위 스코프 (다른 POP 화면과 격리)
### 4. PopActionConfig (액션 설정 타입)
모든 컴포넌트가 사용할 수 있는 액션 표준 구조:
- `type`: "navigate" | "modal" | "save" | "delete" | "api" | "event" | "refresh"
- `navigate`: { screenId, url }
- `modal`: { mode, title, screenId, inlineConfig, modalSize }
- mode: "inline" (설정만으로 구성) | "screen-ref" (별도 화면 참조)
- title: 모달 제목
- screenId: mode가 "screen-ref"일 때 참조할 POP 화면 ID
- inlineConfig: mode가 "inline"일 때 사용할 DataSourceConfig + 표시 설정
- modalSize: { width, height } 모달 크기
- `save`: { targetColumns }
- `delete`: { confirmMessage }
- `api`: { method, endpoint, body }
- `event`: { eventName, payload }
- `refresh`: { targetComponents }
---
## 컴포넌트 정의 (9개)
### 1. pop-text (완성)
- **한 줄 정의**: 보여주기만 함
- **카테고리**: display
- **역할**: 정적 표시 전용 (이벤트 없음)
- **서브타입**: text, datetime, image, title
- **데이터**: 없음 (정적 콘텐츠)
- **이벤트**: 발행 없음, 수신 없음
- **설정**: 내용, 폰트 크기/굵기, 좌우/상하 정렬, 이미지 URL/맞춤/크기, 날짜 포맷 빌더
### 2. pop-dashboard (신규 - 2026-02-09 토의 결과 반영)
- **한 줄 정의**: 여러 집계 아이템을 묶어서 다양한 방식으로 보여줌
- **카테고리**: display
- **역할**: 숫자 데이터를 집계/계산하여 시각화. 하나의 컴포넌트 안에 여러 집계 아이템을 담는 컨테이너
- **구조**: 1개 pop-dashboard = 여러 DashboardItem의 묶음. 각 아이템은 독립적으로 데이터 소스/서브타입/보이기숨기기 설정 가능
- **서브타입** (아이템별로 선택, 한 묶음에 혼합 가능):
- kpi-card: 숫자 + 단위 + 라벨 + 증감 표시
- chart: 막대/원형/라인 차트
- gauge: 게이지 (목표 대비 달성률)
- stat-card: 통계 카드 (건수 + 대기 + 링크)
- **표시 모드** (디자이너가 선택):
- arrows: 좌우 버튼으로 아이템 넘기기
- auto-slide: 전광판처럼 자동 전환 (터치 시 멈춤, 일정 시간 후 재개)
- grid: 컴포넌트 영역 내부를 행/열로 쪼개서 여러 아이템 동시 표시 (디자이너가 각 아이템 위치 직접 지정)
- scroll: 좌우 또는 상하 스와이프
- **데이터**: 각 아이템별 독립 DataSourceConfig (조인/집계 자유)
- **계산식 지원**: "생산량/총재고량", "출고량/현재고량" 같은 복합 표현 가능
- 값 A, B를 각각 다른 테이블/집계로 설정
- 표시 형태: 분수(1,234/5,678), 퍼센트(21.7%), 비율(1,234:5,678)
- **CRUD**: 주로 읽기. 목표값 수정 등 필요 시 write 컬럼으로 저장 가능
- **이벤트**:
- 수신: filter_changed, data_ready
- 발행: kpi_clicked (아이템 클릭 시 상세 데이터 전달)
- **설정**: 데이터 소스(드롭다운 기반 쉬운 집계), 집계 함수, 계산식, 라벨, 단위, 색상 구간, 차트 타입, 새로고침 주기, 목표값, 표시 모드, 아이템별 보이기/숨기기
- **보이기/숨기기**: 각 아이템별로 pop-system에서 개별 on/off 가능 (userConfigurable)
- **기존 POP 대시보드 폐기**: `frontend/components/pop/dashboard/` 폴더 전체를 이 컴포넌트로 대체 예정 (Phase 1~3 완료 후)
#### pop-dashboard 데이터 구조
```
PopDashboardConfig {
items: DashboardItem[] // 아이템 목록 (각각 독립 설정)
displayMode: "arrows" | "auto-slide" | "grid" | "scroll"
autoSlideInterval: number // 자동 슬라이드 간격(초)
gridLayout: { columns: number, rows: number } // 행열 그리드 설정
showIndicator: boolean // 페이지 인디케이터 표시
gap: number // 아이템 간 간격
}
DashboardItem {
id: string
label: string // pop-system에서 보이기/숨기기용 이름
visible: boolean // 보이기/숨기기
subType: "kpi-card" | "chart" | "gauge" | "stat-card"
dataSource: DataSourceConfig // 각 아이템별 독립 데이터 소스
// 행열 그리드 모드에서의 위치 (디자이너가 직접 지정)
gridPosition: { col: number, row: number, colSpan: number, rowSpan: number }
// 계산식 (선택사항)
formula?: {
enabled: boolean
values: [
{ id: "A", dataSource: DataSourceConfig, label: "생산량" },
{ id: "B", dataSource: DataSourceConfig, label: "총재고량" },
]
expression: string // "A / B", "A + B", "A / B * 100"
displayFormat: "value" | "fraction" | "percent" | "ratio"
}
// 서브타입별 설정
kpiConfig?: { unit, colorRanges, showTrend, trendPeriod }
chartConfig?: { chartType, xAxis, yAxis, colors }
gaugeConfig?: { min, max, target, colorRanges }
statConfig?: { categories, showLink }
}
```
#### 설정 패널 흐름 (드롭다운 기반 쉬운 집계)
```
1. [+ 아이템 추가] 버튼 클릭
2. 서브타입 선택: kpi-card / chart / gauge / stat-card
3. 데이터 모드 선택: [단일 집계] 또는 [계산식]
[단일 집계]
- 테이블 선택 (table-schema API로 목록)
- 조인할 테이블 추가 (선택사항)
- 컬럼 선택 → 집계 함수 선택 (합계/건수/평균/최소/최대)
- 필터 조건 추가
[계산식] (예: 생산량/총재고량)
- 값 A: 테이블 -> 컬럼 -> 집계함수
- 값 B: 테이블 -> 컬럼 -> 집계함수 (다른 테이블도 가능)
- 계산식: A / B
- 표시 형태: 분수 / 퍼센트 / 비율
4. 라벨, 단위, 색상 등 외형 설정
5. 행열 그리드 위치 설정 (grid 모드일 때)
```
### 3. pop-table (신규 - 가장 복잡)
- **한 줄 정의**: 데이터 목록을 보여주고 편집함
- **카테고리**: display
- **역할**: 데이터 목록 표시 + 편집 (카드형/테이블형)
- **서브타입**:
- card-list: 카드 형태
- table-list: 테이블 형태 (행/열 장부)
- **데이터**: DataSourceConfig (조인/컬럼별 읽기쓰기 자유)
- **CRUD**: useDataSource의 save/update/delete 사용. write/readwrite 컬럼만 자동 추출
- **카드 템플릿** (card-list 전용): 카드 내부 미니 그리드로 요소 배치, 요소별 데이터 바인딩
- **이벤트**:
- 수신: filter_changed, refresh, data_ready
- 발행: row_selected, row_action, save_complete, delete_complete
- **설정**: 데이터 소스, 표시 모드, 카드 템플릿, 컬럼 정의, 행 선택 방식, 페이징, 정렬, 인라인 편집 여부
### 4. pop-button (신규)
- **한 줄 정의**: 누르면 액션 실행 (저장, 삭제 등)
- **카테고리**: action
- **역할**: 액션 실행 (저장, 삭제, API 호출, 모달 열기 등)
- **데이터**: 이벤트로 수신한 데이터를 액션에 활용
- **CRUD**: 버튼 클릭 시 수신 데이터 기반으로 save/update/delete 실행
- **이벤트**:
- 수신: data_ready, row_selected
- 발행: save_complete, delete_complete 등
- **설정**: 라벨, 아이콘, 크기, 스타일, 액션 설정(PopActionConfig), 확인 다이얼로그, 로딩 상태
### 5. pop-icon (신규)
- **한 줄 정의**: 누르면 어딘가로 이동 (돌아오는 값 없음)
- **카테고리**: action
- **역할**: 네비게이션 (화면 이동, URL 이동)
- **데이터**: 없음
- **이벤트**: 없음 (네비게이션은 이벤트가 아닌 직접 실행)
- **설정**: 아이콘 종류(lucide-icon), 라벨, 배경색/그라디언트, 크기, 클릭 액션(PopActionConfig), 뱃지 표시
- **pop-lookup과의 차이**: pop-icon은 이동/실행만 함. 값을 선택해서 돌려주지 않음
### 6. pop-search (신규)
- **한 줄 정의**: 조건을 입력해서 다른 컴포넌트를 조회/필터링
- **카테고리**: input
- **역할**: 다른 컴포넌트에 필터 조건 전달 + 자체 데이터 조회
- **서브타입**:
- text-search: 텍스트 검색
- date-range: 날짜 범위
- select-filter: 드롭다운 선택 (공통코드 연동)
- combo-filter: 복합 필터 (여러 조건 조합)
- **실행 방식**: auto(값 변경 즉시) 또는 button(검색 버튼 클릭 시)
- **데이터**: 공통코드/카테고리 API로 선택 항목 조회
- **이벤트**:
- 수신: 없음
- 발행: filter_changed (필터 값 변경 시)
- **설정**: 필터 타입, 대상 컬럼, 공통코드 연결, 플레이스홀더, 실행 방식(auto/button), 발행할 이벤트 이름
- **pop-field와의 차이**: pop-search 입력값은 조회용(DB에 안 들어감). pop-field 입력값은 저장용(DB에 들어감)
### 7. pop-field (신규)
- **한 줄 정의**: 저장할 값을 입력
- **카테고리**: input
- **역할**: 단일 데이터 입력 (폼 필드) - 입력한 값이 DB에 저장되는 것이 목적
- **서브타입**:
- text: 텍스트 입력
- number: 숫자 입력 (수량, 금액)
- date: 날짜 선택
- select: 드롭다운 선택
- numpad: 큰 숫자패드 (현장용)
- **데이터**: DataSourceConfig (선택적)
- select 옵션을 DB에서 조회 가능
- ColumnBinding으로 입력값의 저장 대상 테이블/컬럼 지정
- **CRUD**: 자체 저장은 보통 하지 않음. value_changed 이벤트로 pop-button 등에 전달
- **이벤트**:
- 수신: set_value (외부에서 값 설정)
- 발행: value_changed (값 + 컬럼명 + 모드 정보)
- **설정**: 입력 타입, 라벨, 플레이스홀더, 필수 여부, 유효성 검증, 최소/최대값, 단위 표시, 바인딩 컬럼
### 8. pop-lookup (신규)
- **한 줄 정의**: 모달에서 값을 골라서 반환
- **카테고리**: input
- **역할**: 필드를 클릭하면 모달이 열리고, 목록에서 선택하면 값이 반환되는 컴포넌트
- **서브타입 (모달 안 표시 방식)**:
- card: 카드형 목록
- table: 테이블형 목록
- icon-grid: 아이콘 그리드 (참조 화면의 거래처 선택처럼)
- **동작 흐름**: 필드 클릭 -> 모달 열림 -> 목록에서 선택 -> 모달 닫힘 -> 필드에 값 표시 + 이벤트 발행
- **데이터**: DataSourceConfig (모달 안 목록의 데이터 소스)
- **이벤트**:
- 수신: set_value (외부에서 값 초기화)
- 발행: value_selected (선택한 레코드 전체 데이터 전달), filter_changed (선택 값을 필터로 전달)
- **설정**: 라벨, 플레이스홀더, 데이터 소스, 모달 표시 방식(card/table/icon-grid), 표시 컬럼(모달 목록에 보여줄 컬럼), 반환 컬럼(선택 시 돌려줄 값), 발행할 이벤트 이름
- **pop-icon과의 차이**: pop-icon은 이동/실행만 하고 값이 안 돌아옴. pop-lookup은 값을 골라서 돌려줌
- **pop-search와의 차이**: pop-search는 텍스트/날짜/드롭다운으로 필터링. pop-lookup은 모달을 열어서 목록에서 선택
#### pop-lookup 모달 화면 설계 방식
pop-lookup이 열리는 모달의 내부 화면은 **두 가지 방식** 중 선택할 수 있다:
**방식 A: 인라인 모달 (기본)**
- pop-lookup 컴포넌트의 설정 패널에서 직접 모달 내부 화면을 구성
- DataSourceConfig + 표시 컬럼 + 검색 필터 설정만으로 동작
- 별도 화면 생성 없이 컴포넌트 설정만으로 완결
- 적합한 경우: 단순 목록 선택 (거래처 목록, 품목 목록 등)
**방식 B: 외부 화면 참조 (고급)**
- 별도의 POP 화면(screen_id)을 모달로 연결
- 모달 안에서 검색/필터/테이블 등 복잡한 화면을 디자이너로 자유롭게 구성
- 여러 pop-lookup에서 같은 모달 화면을 재사용 가능
- 적합한 경우: 복잡한 검색/필터가 필요한 선택 화면, 여러 화면에서 공유하는 모달
**설정 구조:**
```
modalConfig: {
mode: "inline" | "screen-ref"
// mode = "inline"일 때 사용
dataSource: DataSourceConfig
displayColumns: ColumnBinding[]
searchFilter: { enabled: boolean, targetColumns: string[] }
modalSize: { width: number, height: number }
// mode = "screen-ref"일 때 사용
screenId: number // 참조할 POP 화면 ID
returnMapping: { // 모달 화면에서 선택된 값을 어떻게 매핑할지
sourceColumn: string // 모달 화면에서 반환하는 컬럼
targetField: string // pop-lookup 필드에 표시할 값
}[]
modalSize: { width: number, height: number }
}
```
**기존 시스템과의 호환성 (검증 완료):**
| 항목 | 현재 상태 | pop-lookup 지원 여부 |
|------|-----------|---------------------|
| DB: layout_data JSONB | 유연한 JSON 구조 | modalConfig를 layout_data에 저장 가능 (스키마 변경 불필요) |
| DB: screen_layouts_pop 테이블 | screen_id + company_code 기반 | 모달 화면도 별도 screen_id로 저장 가능 |
| 프론트: TabsWidget | screenId로 외부 화면 참조 지원 | 같은 패턴으로 모달에서 외부 화면 로드 가능 |
| 프론트: detectLinkedModals API | 연결된 모달 화면 감지 기능 있음 | 화면 간 참조 관계 추적에 활용 가능 |
| 백엔드: saveLayoutPop/getLayoutPop | POP 전용 저장/조회 API 있음 | 모달 화면도 동일 API로 저장/조회 가능 |
| 레이어 시스템 | layer_id 기반 다중 레이어 지원 | 모달 내부 레이아웃을 레이어로 관리 가능 |
**DB 마이그레이션 불필요**: layout_data가 JSONB이므로 modalConfig를 컴포넌트 overrides에 포함하면 됨.
**백엔드 변경 불필요**: 기존 saveLayoutPop/getLayoutPop API가 그대로 사용 가능.
**프론트엔드 참고 패턴**: TabsWidget의 screenId 참조 방식을 그대로 차용.
### 9. pop-system (신규)
- **한 줄 정의**: 시스템 설정을 하나로 통합한 컴포넌트 (프로필, 테마, 보이기/숨기기)
- **카테고리**: system
- **역할**: 사용자 개인 설정 기능을 제공하는 통합 컴포넌트
- **내부 포함 기능**:
- 프로필 표시 (사용자명, 부서)
- 테마 선택 (기본/다크/블루/그린)
- 대시보드 보이기/숨기기 체크박스 (같은 화면의 userConfigurable=true 컴포넌트를 자동 수집)
- 하단 메뉴 보이기/숨기기
- 드래그앤드롭으로 순서 변경
- **디자이너가 설정하는 것**: 크기(그리드에서 차지하는 영역), 내부 라벨/아이콘 크기와 위치
- **사용자가 하는 것**: 체크박스로 컴포넌트 보이기/숨기기, 테마 선택, 순서 변경
- **데이터**: 같은 화면의 layout_data에서 컴포넌트 목록을 자동 수집
- **저장**: 사용자별 설정을 localStorage에 저장 (데스크탑 패턴 따름)
- **이벤트**:
- 수신: 없음
- 발행: visibility_changed (컴포넌트 보이기/숨기기 변경 시), theme_changed (테마 변경 시)
- **설정**: 내부 라벨 크기, 아이콘 크기, 위치 정도만
- **특이사항**:
- 디자이너가 이 컴포넌트를 배치하지 않으면 해당 화면에 개인 설정 기능이 없다
- 디자이너가 "이 화면에 설정 기능을 넣을지 말지"를 직접 결정하는 구조
- 메인 홈에는 배치, 업무 화면(입고 등)에는 안 배치하는 식으로 사용
---
## 컴포넌트 간 통신 예시
### 예시 1: 검색 -> 필터 연동
```mermaid
sequenceDiagram
participant Search as pop-search
participant Dashboard as pop-dashboard
participant Table as pop-table
Note over Search: 사용자가 창고 WH01 선택
Search->>Dashboard: filter_changed
Search->>Table: filter_changed
Note over Dashboard: DataSource 재조회
Note over Table: DataSource 재조회
```
### 예시 2: 데이터 전달 + 선택적 저장
```mermaid
sequenceDiagram
participant Table as pop-table
participant Field as pop-field
participant Button as pop-button
Note over Table: 사용자가 발주 행 선택
Table->>Field: row_selected
Table->>Button: row_selected
Note over Field: 사용자가 qty를 500으로 입력
Field->>Button: value_changed
Note over Button: 사용자가 저장 클릭
Note over Button: write/readwrite 컬럼만 추출하여 저장
Button->>Table: save_complete
Note over Table: 데이터 새로고침
```
### 예시 3: pop-lookup 거래처 선택 -> 품목 조회
```mermaid
sequenceDiagram
participant Lookup as pop-lookup
participant Table as pop-table
Note over Lookup: 사용자가 거래처 필드 클릭
Note over Lookup: 모달 열림 - 거래처 목록 표시
Note over Lookup: 사용자가 대한금속 선택
Note over Lookup: 모달 닫힘 - 필드에 대한금속 표시
Lookup->>Table: filter_changed { company: "대한금속" }
Note over Table: company=대한금속 필터로 재조회
Note over Table: 발주 품목 3건 표시
```
### 예시 4: pop-lookup 인라인 모달 vs 외부 화면 참조
```mermaid
sequenceDiagram
participant User as 사용자
participant Lookup as pop-lookup (거래처)
participant Modal as 모달
Note over User,Modal: [방식 A: 인라인 모달]
User->>Lookup: 거래처 필드 클릭
Lookup->>Modal: 인라인 모달 열림 (DataSourceConfig 기반)
Note over Modal: supplier 테이블에서 목록 조회
Note over Modal: 테이블형 목록 표시
User->>Modal: "대한금속" 선택
Modal->>Lookup: value_selected { supplier_code: "DH001", name: "대한금속" }
Note over Lookup: 필드에 "대한금속" 표시
Note over User,Modal: [방식 B: 외부 화면 참조]
User->>Lookup: 거래처 필드 클릭
Lookup->>Modal: 모달 열림 (screenId=42 화면 로드)
Note over Modal: 별도 POP 화면 렌더링
Note over Modal: pop-search(검색) + pop-table(목록) 등 배치된 컴포넌트 동작
User->>Modal: 검색 후 "대한금속" 선택
Modal->>Lookup: returnMapping 기반으로 값 반환
Note over Lookup: 필드에 "대한금속" 표시
```
### 예시 5: 컬럼별 읽기/쓰기 분리 동작
5개 컬럼이 있는 발주 화면:
- item_code (read) -> 화면에 표시, 저장 안 함
- item_name (read, 조인) -> item_info 테이블에서 가져옴, 저장 안 함
- inbound_qty (readwrite) -> 화면에 표시 + 사용자 수정 + 저장
- warehouse (write) -> 사용자 입력 + 저장
- memo (write) -> 사용자 입력 + 저장
저장 API 호출 시: `{ inbound_qty: 500, warehouse: "WH01", memo: "긴급" }` 만 전달
조회 API 호출 시: 5개 컬럼 전부 + 조인된 item_name까지 조회
---
## 구현 우선순위
- Phase 0 (공통 인프라): ColumnBinding, JoinConfig, DataSourceConfig 타입, useDataSource 훅 (CRUD 포함), usePopEvent 훅 (데이터 전달 포함), PopActionConfig 타입
- Phase 1 (기본 표시): pop-dashboard (4개 서브타입 전부 + 멀티 아이템 컨테이너 + 4개 표시 모드 + 계산식)
- Phase 2 (기본 액션): pop-button, pop-icon
- Phase 3 (데이터 목록): pop-table (테이블형부터, 카드형은 후순위)
- Phase 4 (입력/연동): pop-search, pop-field, pop-lookup
- Phase 5 (고도화): pop-table 카드 템플릿
- Phase 6 (시스템): pop-system (프로필, 테마, 대시보드 보이기/숨기기 통합)
### Phase 1 상세 변경 (2026-02-09 토의 결정)
기존 계획에서 "KPI 카드 우선"이었으나, 토의 결과 **4개 서브타입 전부를 Phase 1에서 구현**으로 변경:
- kpi-card, chart, gauge, stat-card 모두 Phase 1
- 멀티 아이템 컨테이너 (arrows, auto-slide, grid, scroll)
- 계산식 지원 (formula)
- 드롭다운 기반 쉬운 집계 설정
- 기존 `frontend/components/pop/dashboard/` 폴더는 Phase 1 완료 후 폐기/삭제
### 백엔드 API 현황 (호환성 점검 완료)
기존 백엔드에 이미 구현되어 있어 새로 만들 필요 없는 API:
| API | 용도 | 비고 |
|-----|------|------|
| `dataApi.getTableData()` | 동적 테이블 조회 | 페이징, 검색, 정렬, 필터 |
| `dataApi.getJoinedData()` | 2개 테이블 조인 | Entity 조인, 필터링, 중복제거 |
| `entityJoinApi.getTableDataWithJoins()` | Entity 조인 전용 | ID->이름 자동 변환 |
| `dataApi.createRecord/updateRecord/deleteRecord()` | 동적 CRUD | - |
| `dataApi.upsertGroupedRecords()` | 그룹 UPSERT | - |
| `dashboardApi.executeQuery()` | SELECT SQL 직접 실행 | 집계/복합조인용 |
| `dashboardApi.getTableSchema()` | 테이블/컬럼 목록 | 설정 패널 드롭다운용 |
**백엔드 신규 개발 불필요** - 기존 API만으로 모든 데이터 연동 가능
### useDataSource의 API 선택 전략
```
단순 조회 (조인/집계 없음) -> dataApi.getTableData() 또는 entityJoinApi
2개 테이블 조인 -> dataApi.getJoinedData()
3개+ 테이블 조인 또는 집계 -> DataSourceConfig를 SQL로 변환 -> dashboardApi.executeQuery()
CRUD -> dataApi.createRecord/updateRecord/deleteRecord()
```
### POP 전용 훅 분리 (2026-02-09 결정)
데스크탑과의 완전 분리를 위해 POP 전용 훅은 별도 폴더:
- `frontend/hooks/pop/usePopEvent.ts` (POP 전용)
- `frontend/hooks/pop/useDataSource.ts` (POP 전용)
## 기존 시스템 호환성 검증 결과 (v8.0 추가)
v8.0에서 추가된 모달 설계 방식에 대해 기존 시스템과의 호환성을 검증한 결과:
### DB 스키마 (변경 불필요)
| 테이블 | 현재 구조 | 호환성 |
|--------|-----------|--------|
| screen_layouts_v2 | layout_data JSONB + screen_id + company_code + layer_id | modalConfig를 컴포넌트 overrides에 포함하면 됨 |
| screen_layouts_pop | 동일 구조 (POP 전용) | 모달 화면도 별도 screen_id로 저장 가능 |
- layout_data가 JSONB 타입이므로 어떤 JSON 구조든 저장 가능
- 모달 화면을 별도 screen_id로 만들어도 기존 UNIQUE(screen_id, company_code, layer_id) 제약조건과 충돌 없음
- DB 마이그레이션 불필요
### 백엔드 API (변경 불필요)
| API | 엔드포인트 | 호환성 |
|-----|-----------|--------|
| POP 레이아웃 저장 | POST /api/screen-management/screens/:screenId/layout-pop | 모달 화면도 동일 API로 저장 |
| POP 레이아웃 조회 | GET /api/screen-management/screens/:screenId/layout-pop | 모달 화면도 동일 API로 조회 |
| 연결 모달 감지 | detectLinkedModals(screenId) | 화면 간 참조 관계 추적에 활용 |
### 프론트엔드 (참고 패턴 존재)
| 기존 기능 | 위치 | 활용 방안 |
|-----------|------|-----------|
| TabsWidget screenId 참조 | frontend/components/screen/widgets/TabsWidget.tsx | 같은 패턴으로 모달에서 외부 화면 로드 |
| TabsConfigPanel | frontend/components/screen/config-panels/TabsConfigPanel.tsx | pop-lookup 설정 패널의 모달 화면 선택 UI 참조 |
| ScreenDesigner 탭 내부 컴포넌트 | frontend/components/screen/ScreenDesigner.tsx | 모달 내부 컴포넌트 편집 패턴 참조 |
### 결론
- DB 마이그레이션: 불필요
- 백엔드 변경: 불필요
- 프론트엔드: pop-lookup 컴포넌트 구현 시 기존 TabsWidget의 screenId 참조 패턴을 그대로 차용
- 새로운 API: 불필요 (기존 saveLayoutPop/getLayoutPop로 충분)
## 참고 파일
- 레지스트리: `frontend/lib/registry/PopComponentRegistry.ts`
- 기존 텍스트 컴포넌트: `frontend/lib/registry/pop-components/pop-text.tsx`
- 공통 스타일 타입: `frontend/lib/registry/pop-components/types.ts`
- POP 타입 정의: `frontend/components/pop/designer/types/pop-layout.ts`
- 기존 스펙 (v4): `popdocs/components-spec.md`
- 탭 위젯 (모달 참조 패턴): `frontend/components/screen/widgets/TabsWidget.tsx`
- POP 레이아웃 API: `frontend/lib/api/screen.ts` (saveLayoutPop, getLayoutPop)
- 백엔드 화면관리: `backend-node/src/controllers/screenManagementController.ts`

519
README.md
View File

@ -1,472 +1,175 @@
# WACE 솔루션 (PLM) # WACE 솔루션 (ERP/PLM)
## 프로젝트 개요 ## 프로젝트 개요
본 프로젝트는 제품 수명 주기 관리(PLM - Product Lifecycle Management) 솔루션입니다. 본 프로젝트는 WACE ERP/PLM(Product Lifecycle Management) 솔루션입니다.
**기존 JSP 기반 프론트엔드를 Next.js 14로 완전 전환**하여 현대적이고 사용자 친화적인 웹 애플리케이션을 제공합니다. Node.js + Next.js 기반 풀스택 웹 애플리케이션으로, 멀티테넌시를 지원합니다.
## 🚀 주요 특징 ## 주요 특징
- **모던 프론트엔드**: Next.js 14 + TypeScript + shadcn/ui - **모던 프론트엔드**: Next.js (App Router) + TypeScript + shadcn/ui
- **Node.js 백엔드**: Express + TypeScript + PostgreSQL
- **반응형 디자인**: 데스크톱, 태블릿, 모바일 모든 기기 지원 - **반응형 디자인**: 데스크톱, 태블릿, 모바일 모든 기기 지원
- **안정적인 백엔드**: 검증된 Java Spring + MyBatis 기반 API - **멀티테넌시**: 회사별 데이터 격리 (company_code 기반)
- **Docker 기반 배포**: 개발/운영 환경 일관성 보장 - **Docker 기반 배포**: 개발/운영 환경 일관성 보장
- **타입 안전성**: TypeScript로 런타임 에러 방지 - **타입 안전성**: TypeScript로 런타임 에러 방지
## 🛠️ 기술 스택 ## 기술 스택
### Frontend (Next.js 14) ### Frontend
- **프레임워크**: Next.js 14 (App Router) - **프레임워크**: Next.js (App Router, Turbopack)
- **언어**: TypeScript - **언어**: TypeScript
- **UI 라이브러리**: shadcn/ui + Radix UI - **UI 라이브러리**: shadcn/ui + Radix UI
- **스타일링**: Tailwind CSS - **스타일링**: Tailwind CSS
- **폼 처리**: React Hook Form + Zod - **상태 관리**: TanStack Query + React Context
- **상태 관리**: React Context + Hooks
- **아이콘**: Lucide React - **아이콘**: Lucide React
### Backend (기존 유지) ### Backend
- **언어**: Java 7 - **런타임**: Node.js 20+
- **프레임워크**: Spring Framework 3.2.4 - **프레임워크**: Express 4
- **ORM**: MyBatis 3.2.3 - **언어**: TypeScript
- **데이터베이스**: PostgreSQL - **데이터베이스**: PostgreSQL (pg 드라이버)
- **WAS**: Apache Tomcat 7.0 - **인증**: JWT (jsonwebtoken) + bcryptjs
- **로깅**: Winston
### 개발 도구 ### 개발 도구
- **컨테이너화**: Docker + Docker Compose - **컨테이너화**: Docker + Docker Compose
- **코드 품질**: ESLint + TypeScript - **코드 품질**: ESLint + Prettier
- **패키지 관리**: npm - **테스트**: Jest + Supertest
- **백엔드 핫리로드**: nodemon
- **CI/CD**: Jenkins
## 📁 프로젝트 구조 ## 프로젝트 구조
``` ```
new_ph/ ERP-node/
├── frontend/ # Next.js 프론트엔드 ├── backend-node/ # Express + TypeScript 백엔드
│ ├── app/ # Next.js App Router │ ├── src/
│ │ ├── (auth)/ # 인증 관련 페이지 │ │ ├── app.ts # 엔트리포인트
│ │ │ └── login/ # 로그인 페이지 │ │ ├── controllers/ # API 컨트롤러
│ │ ├── dashboard/ # 대시보드 │ │ ├── services/ # 비즈니스 로직
│ │ └── layout.tsx # 루트 레이아웃 │ │ ├── middleware/ # 인증, 에러처리 미들웨어
│ ├── components/ # 재사용 컴포넌트 │ │ ├── routes/ # 라우터
│ │ └── config/ # DB 연결 등 설정
│ └── package.json
├── frontend/ # Next.js 프론트엔드
│ ├── app/ # App Router 페이지
│ ├── components/ # React 컴포넌트
│ │ ├── ui/ # shadcn/ui 기본 컴포넌트 │ │ ├── ui/ # shadcn/ui 기본 컴포넌트
│ │ └── layout/ # 레이아웃 컴포넌트 │ │ ├── admin/ # 관리자 컴포넌트
│ ├── lib/ # 유틸리티 함수 │ │ ├── screen/ # 화면 디자이너
│ └── types/ # TypeScript 타입 정의 │ │ └── v2/ # V2 컴포넌트
├── src/ # Java 백엔드 소스 │ ├── lib/ # 유틸리티, API 클라이언트
│ └── com/pms/ # 패키지 구조 │ ├── hooks/ # Custom React Hooks
├── WebContent/ # 레거시 JSP (사용 중단) │ └── package.json
├── db/ # 데이터베이스 스크립트 ├── db/ # 데이터베이스
└── docker-compose.win.yml # Windows 환경 설정 │ └── migrations/ # 순차 마이그레이션 SQL
├── docker/ # Docker 설정 (dev/prod/deploy)
├── scripts/ # 개발/배포 스크립트
├── docs/ # 프로젝트 문서
├── Dockerfile # 프로덕션 멀티스테이지 빌드
├── Jenkinsfile # CI/CD 파이프라인
└── .cursorrules # AI 개발 가이드
``` ```
## 🚀 빠른 시작 ## 빠른 시작
### 1. 필수 요구사항 ### 1. 필수 요구사항
- **Docker Desktop** (Windows/Mac) 또는 **Docker + Docker Compose** (Linux) - **Node.js**: 20.10+
- **Git** (소스 코드 관리) - **PostgreSQL**: 데이터베이스 서버
- **npm**: 10.0+
### 2. Windows 환경에서 실행 ### 2. 개발 환경 실행
#### 자동 실행 (권장)
```bash ```bash
# 프로젝트 시작 # 백엔드 (nodemon으로 자동 재시작)
./run-windows.bat cd backend-node && npm install && npm run dev
# 서비스 상태 확인 # 프론트엔드 (Turbopack)
./status-windows.bat cd frontend && npm install && npm run dev
# 서비스 중지
./stop-windows.bat
``` ```
#### 수동 실행 ### 3. Docker 환경 실행
```bash ```bash
# Docker 컨테이너 실행 # 백엔드 + 프론트엔드 (개발)
docker-compose -f docker-compose.win.yml up --build -d docker-compose -f docker-compose.backend.win.yml up -d
docker-compose -f docker-compose.frontend.win.yml up -d
# 로그 확인 # 프로덕션 배포
docker-compose -f docker-compose.win.yml logs -f docker-compose -f docker/deploy/docker-compose.yml up -d
``` ```
### 3. 서비스 접속 ### 4. 서비스 접속
| 서비스 | URL | 설명 | | 서비스 | URL | 설명 |
| -------------- | --------------------- | ------------------------------ | | -------------- | --------------------- | ------------------------------ |
| **프론트엔드** | http://localhost:9771 | Next.js 기반 사용자 인터페이스 | | **프론트엔드** | http://localhost:9771 | Next.js 사용자 인터페이스 |
| **백엔드 API** | http://localhost:9090 | Spring 기반 REST API | | **백엔드 API** | http://localhost:8080 | Express REST API |
> **주의**: 기존 JSP 화면(`localhost:9090`)은 더 이상 사용하지 않습니다. ## 주요 기능
> 모든 사용자는 **Next.js 프론트엔드(`localhost:9771`)**를 사용해주세요.
## 🎨 UI/UX 디자인 시스템 ### 1. 사용자 및 권한 관리
- 사용자 계정 관리 (CRUD)
- 역할 기반 접근 제어 (RBAC)
- 부서/조직 관리
- 멀티테넌시 (회사별 데이터 격리)
### shadcn/ui 컴포넌트 라이브러리 ### 2. 메뉴 및 화면 관리
- 동적 메뉴 구성
- 화면 디자이너 (드래그앤드롭)
- V2 컴포넌트 시스템
- **일관된 디자인**: 전체 애플리케이션에서 통일된 UI 컴포넌트 ### 3. 플로우(워크플로우) 관리
- **접근성**: WCAG 가이드라인 준수 - 비즈니스 프로세스 정의
- **커스터마이징**: Tailwind CSS로 쉬운 스타일 변경 - 데이터 흐름 관리
- **다크모드**: 자동 테마 전환 지원 - 감사 로그
### 공통 스타일 가이드 ### 4. 제품/BOM 관리
- BOM 구성 및 버전 관리
- 제품 정보 관리
```typescript ### 5. 기타
// 색상 팔레트 - 파일/문서 관리
const colors = { - 메일 연동
primary: "hsl(222.2 47.4% 11.2%)", // 네이비 블루 - 외부 DB 연결
secondary: "hsl(210 40% 96%)", // 연한 그레이 - 번호 채번 규칙
accent: "hsl(210 40% 98%)", // 거의 화이트
destructive: "hsl(0 62.8% 30.6%)", // 레드
muted: "hsl(210 40% 96%)", // 음소거된 그레이
};
// 타이포그래피 ## 환경 변수
const typography = {
fontFamily: "Inter, system-ui, sans-serif",
fontSize: {
xs: "0.75rem", // 12px
sm: "0.875rem", // 14px
base: "1rem", // 16px
lg: "1.125rem", // 18px
xl: "1.25rem", // 20px
},
};
```
## 🔧 개발 가이드
### 컴포넌트 개발 원칙
#### 1. 재사용 가능한 컴포넌트
```typescript
// components/ui/button.tsx
interface ButtonProps {
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost";
size?: "default" | "sm" | "lg" | "icon";
children: React.ReactNode;
}
export function Button({
variant = "default",
size = "default",
children,
...props
}: ButtonProps) {
return (
<button className={cn(buttonVariants({ variant, size }))} {...props}>
{children}
</button>
);
}
```
#### 2. 폼 컴포넌트
```typescript
// React Hook Form + Zod 사용
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
const loginSchema = z.object({
userId: z.string().min(1, "사용자 ID를 입력해주세요"),
password: z.string().min(1, "비밀번호를 입력해주세요"),
});
export function LoginForm() {
const form = useForm<z.infer<typeof loginSchema>>({
resolver: zodResolver(loginSchema),
});
// 폼 처리 로직
}
```
#### 3. API 연동
```typescript
// lib/api.ts
class ApiClient {
private baseURL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:9090";
async login(credentials: LoginCredentials): Promise<LoginResponse> {
const response = await fetch(`${this.baseURL}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(credentials),
credentials: "include", // 세션 쿠키 포함
});
if (!response.ok) throw new Error("로그인 실패");
return response.json();
}
}
```
### 스타일링 가이드
#### 1. Tailwind CSS 클래스
```typescript
// 일반적인 레이아웃
<div className="flex items-center justify-center min-h-screen bg-slate-50">
<div className="w-full max-w-md space-y-6">
{/* 컨텐츠 */}
</div>
</div>
// 카드 컴포넌트
<div className="bg-white rounded-lg shadow-lg border border-slate-200 p-6">
{/* 카드 내용 */}
</div>
// 반응형 그리드
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* 그리드 아이템 */}
</div>
```
#### 2. CSS 변수 활용
```css
/* globals.css */
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222.2 84% 4.9%;
}
```
## 🔐 인증 시스템
### 세션 기반 인증 (기존 백엔드 호환)
```typescript
// 로그인 프로세스
1. 사용자가 로그인 폼 제출
2. Next.js에서 백엔드 API 호출
3. 백엔드에서 세션 생성 (기존 로직 사용)
4. 프론트엔드에서 인증 상태 관리
5. 보호된 라우트 접근 제어
```
### 라우트 보호
```typescript
// middleware.ts
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 공개 페이지
const publicPaths = ["/login", "/"];
if (publicPaths.includes(pathname)) return NextResponse.next();
// 인증 확인
const sessionCookie = request.cookies.get("JSESSIONID");
if (!sessionCookie) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
```
## 📊 주요 기능
### 1. 대시보드
- **프로젝트 현황**: 진행 중인 프로젝트 상태 모니터링
- **작업 요약**: 개인별 할당된 작업 목록
- **알림 센터**: 중요한 업데이트 및 알림
- **차트 및 그래프**: 프로젝트 진척도 시각화
### 2. 프로젝트 관리
- **프로젝트 생성/수정**: 새 프로젝트 등록 및 정보 관리
- **팀 구성**: 프로젝트 멤버 할당 및 역할 관리
- **마일스톤**: 주요 일정 및 목표 설정
- **진행 상황 추적**: 실시간 프로젝트 진척도 모니터링
### 3. 제품 관리
- **제품 카탈로그**: 제품 정보 및 사양 관리
- **BOM 관리**: Bill of Materials 구성 및 버전 관리
- **설계 변경**: ECO/ECR 프로세스 관리
- **문서 관리**: 기술 문서 및 도면 버전 제어
### 4. 사용자 관리
- **사용자 계정**: 계정 생성/수정/비활성화
- **권한 관리**: 역할 기반 접근 제어 (RBAC)
- **부서 관리**: 조직 구조 및 부서별 권한 설정
- **감사 로그**: 사용자 활동 추적 및 보안 모니터링
## 🚢 배포 가이드
### 개발 환경
```bash ```bash
# 프론트엔드 개발 서버 # backend-node/.env
cd frontend && npm run dev DATABASE_URL=postgresql://postgres:password@localhost:5432/dbname
JWT_SECRET=your-jwt-secret
JWT_EXPIRES_IN=24h
PORT=8080
CORS_ORIGIN=http://localhost:9771
# 백엔드 (Docker) # frontend/.env.local
docker-compose -f docker-compose.win.yml up -d plm-app NEXT_PUBLIC_API_URL=http://localhost:8080/api
``` ```
### 운영 환경 ## 배포
### 프로덕션 빌드
```bash ```bash
# 전체 서비스 배포 # 멀티스테이지 Docker 빌드 (백엔드 + 프론트엔드)
docker-compose -f docker-compose.prod.yml up -d docker build -t wace-solution .
# 무중단 배포 (blue-green)
./deploy-production.sh
``` ```
### 환경 변수 설정 ### CI/CD
```bash Jenkins 파이프라인 (`Jenkinsfile`)으로 자동 빌드 및 배포가 설정되어 있습니다.
# .env.local (Next.js)
NEXT_PUBLIC_API_URL=http://localhost:9090
NEXT_PUBLIC_APP_ENV=development
# docker-compose.win.yml (백엔드) ## 코드 컨벤션
DB_URL=jdbc:postgresql://db:5432/plm
DB_USERNAME=postgres
DB_PASSWORD=secure_password
```
## 🔧 문제 해결
### 자주 발생하는 문제
#### 1. 로그인 화면이 업데이트되지 않는 경우
```bash
# 브라우저 캐시 클리어 후 다음 확인:
# - Next.js 서버 재시작
npm run dev
# - 올바른 URL 접속 확인
# 올바름: http://localhost:9771/login
# 잘못됨: http://localhost:9090/login.jsp
```
#### 2. Docker 컨테이너 실행 오류
```bash
# 포트 충돌 확인
netstat -ano | findstr :9771
netstat -ano | findstr :9090
# Docker 시스템 정리
docker system prune -a
docker-compose -f docker-compose.win.yml down --volumes
```
#### 3. API 연결 오류
```bash
# 백엔드 컨테이너 로그 확인
docker-compose -f docker-compose.win.yml logs plm-app
# 네트워크 연결 확인
curl http://localhost:9090/api/health
```
### 개발자 도구
#### 브라우저 개발자 도구
- **Console**: JavaScript 오류 확인
- **Network**: API 요청/응답 모니터링
- **Application**: 세션 쿠키 확인
#### 로그 확인
```bash
# Next.js 개발 서버 로그
npm run dev
# 백엔드 애플리케이션 로그
docker-compose -f docker-compose.win.yml logs -f plm-app
# 데이터베이스 로그
docker-compose -f docker-compose.win.yml logs -f db
```
## 📈 성능 최적화
### Next.js 최적화
- **이미지 최적화**: Next.js Image 컴포넌트 사용
- **코드 분할**: 동적 임포트로 번들 크기 최소화
- **서버 사이드 렌더링**: 초기 로딩 속도 개선
- **정적 생성**: 변경되지 않는 페이지 사전 생성
### 백엔드 최적화
- **데이터베이스 인덱스**: 자주 조회되는 필드 인덱싱
- **쿼리 최적화**: N+1 문제 해결
- **캐싱**: Redis를 통한 세션 및 데이터 캐싱
- **리소스 최적화**: JVM 메모리 튜닝
## 🤝 기여 가이드
### 코드 컨벤션
- **TypeScript**: 엄격한 타입 정의 사용 - **TypeScript**: 엄격한 타입 정의 사용
- **ESLint**: 코드 품질 유지. - **ESLint + Prettier**: 일관된 코드 스타일
- **Prettier**: 일관된 코드 포맷팅 - **shadcn/ui**: UI 컴포넌트 표준
- **커밋 메시지**: Conventional Commits 규칙 준수 - **API 클라이언트**: `frontend/lib/api/` 전용 클라이언트 사용 (fetch 직접 사용 금지)
- **멀티테넌시**: 모든 쿼리에 company_code 필터링 필수
### 브랜치 전략
```bash
main # 운영 환경 배포 브랜치
develop # 개발 환경 통합 브랜치
feature/* # 기능 개발 브랜치
hotfix/* # 긴급 수정 브랜치
```
### Pull Request 프로세스
1. 기능 브랜치에서 개발
2. 테스트 코드 작성
3. PR 생성 및 코드 리뷰
4. 승인 후 develop 브랜치에 병합
## 📞 지원 및 문의
- **개발팀 문의**: 내부 Slack 채널 `#plm-support`
- **버그 리포트**: GitHub Issues
- **기능 요청**: Product Owner와 협의
- **긴급 상황**: 개발팀 직접 연락
---
## 📝 변경 이력
### v2.0.0 (2025년 1월)
- ✅ JSP → Next.js 14 완전 전환
- ✅ shadcn/ui 디자인 시스템 도입
- ✅ TypeScript 타입 안전성 강화
- ✅ 반응형 디자인 적용
- ✅ WACE 브랜딩 적용
### v1.x (레거시)
- ❌ JSP + jQuery 기반 (사용 중단)
- ❌ 데스크톱 전용 UI
- ❌ 제한적인 확장성
**🎉 현재 버전 2.0.0에서는 완전히 새로운 사용자 경험을 제공합니다!**

View File

@ -1,3 +0,0 @@
<Context docBase="ilshin" path="/" reloadable="true" source="org.eclipse.jst.jee.server:ilshin">
<Resource auth="Container" driverClassName="org.postgresql.Driver" maxActive="100" maxIdle="10" maxWait="-1" name="plm" password="admin0909!!" type="javax.sql.DataSource" url="jdbc:postgresql://211.224.136.4:5432/ilshin" username="postgres"/>
</Context>

46
STATUS.md Normal file
View File

@ -0,0 +1,46 @@
# 프로젝트 상태 추적
> **최종 업데이트**: 2026-02-11
---
## 현재 진행 중
### pop-dashboard 스타일 정리
**상태**: 코딩 완료, 브라우저 확인 대기
**계획서**: [popdocs/PLAN.md](./popdocs/PLAN.md)
**내용**: 글자 크기 커스텀 제거 + 라벨 정렬만 유지 + stale closure 수정
---
## 다음 작업
| 순서 | 작업 | 상태 |
|------|------|------|
| 1 | pop-card-list 입력 필드/계산 필드 구조 개편 (PLAN.MD 참고) | [ ] 코딩 대기 |
| 2 | pop-card-list 담기 버튼 독립화 (보류) | [ ] 대기 |
| 3 | pop-card-list 반응형 표시 런타임 적용 | [ ] 대기 |
---
## 완료된 작업 (최근)
| 날짜 | 작업 | 비고 |
|------|------|------|
| 2026-02-11 | 대시보드 스타일 정리 | FONT_SIZE_PX/글자 크기 Select 삭제, ItemStyleConfig -> labelAlign만, stale closure 수정 |
| 2026-02-10 | 디자이너 캔버스 UX 개선 | 헤더 제거, 실제 데이터 렌더링, 컴포넌트 목록 |
| 2026-02-10 | 차트/게이지/네비게이션/정렬 디자인 개선 | CartesianGrid, abbreviateNumber, 오버레이 화살표/인디케이터 |
| 2026-02-10 | 대시보드 4가지 아이템 모드 완성 | groupBy UI, xAxisColumn, 통계카드 카테고리, 필터 버그 수정 |
| 2026-02-09 | POP 뷰어 스크롤 수정 | overflow-hidden 제거, overflow-auto 공통 적용 |
| 2026-02-09 | POP 뷰어 실제 컴포넌트 렌더링 | 레지스트리 초기화 + renderActualComponent |
| 2026-02-08 | V2/V2 컴포넌트 스키마 정비 | componentConfig.ts 통합 관리 |
---
## 알려진 이슈
| # | 이슈 | 심각도 | 상태 |
|---|------|--------|------|
| 1 | KPI 증감율(trendValue) 미구현 | 낮음 | 향후 구현 |
| 2 | 게이지 동적 목표값(targetDataSource) 미구현 | 낮음 | 향후 구현 |
| 3 | 기존 저장 데이터의 `itemStyle.align``labelAlign`으로 마이그레이션 안 됨 | 낮음 | 이전에 작동 안 했으므로 실질 영향 없음 |

View File

@ -1,3 +0,0 @@
Manifest-Version: 1.0
Class-Path:

View File

@ -1,74 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="ko" xml:lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>네이버 :: Smart Editor 2 &#8482;</title>
<script type="text/javascript" src="./js/HuskyEZCreator.js" charset="utf-8"></script>
</head>
<body>
<form action="sample.php" method="post">
<textarea name="ir1" id="ir1" rows="10" cols="100" style="width:766px; height:412px; display:none;"></textarea>
<!--textarea name="ir1" id="ir1" rows="10" cols="100" style="width:100%; height:412px; min-width:610px; display:none;"></textarea-->
<p>
<input type="button" onclick="pasteHTML();" value="본문에 내용 넣기" />
<input type="button" onclick="showHTML();" value="본문 내용 가져오기" />
<input type="button" onclick="submitContents(this);" value="서버로 내용 전송" />
<input type="button" onclick="setDefaultFont();" value="기본 폰트 지정하기 (궁서_24)" />
</p>
</form>
<script type="text/javascript">
var oEditors = [];
// 추가 글꼴 목록
//var aAdditionalFontSet = [["MS UI Gothic", "MS UI Gothic"], ["Comic Sans MS", "Comic Sans MS"],["TEST","TEST"]];
nhn.husky.EZCreator.createInIFrame({
oAppRef: oEditors,
elPlaceHolder: "ir1",
sSkinURI: "SmartEditor2Skin.html",
htParams : {
bUseToolbar : true, // 툴바 사용 여부 (true:사용/ false:사용하지 않음)
bUseVerticalResizer : true, // 입력창 크기 조절바 사용 여부 (true:사용/ false:사용하지 않음)
bUseModeChanger : true, // 모드 탭(Editor | HTML | TEXT) 사용 여부 (true:사용/ false:사용하지 않음)
//aAdditionalFontList : aAdditionalFontSet, // 추가 글꼴 목록
fOnBeforeUnload : function(){
//alert("완료!");
}
}, //boolean
fOnAppLoad : function(){
//예제 코드
//oEditors.getById["ir1"].exec("PASTE_HTML", ["로딩이 완료된 후에 본문에 삽입되는 text입니다."]);
},
fCreator: "createSEditor2"
});
function pasteHTML() {
var sHTML = "<span style='color:#FF0000;'>이미지도 같은 방식으로 삽입합니다.<\/span>";
oEditors.getById["ir1"].exec("PASTE_HTML", [sHTML]);
}
function showHTML() {
var sHTML = oEditors.getById["ir1"].getIR();
alert(sHTML);
}
function submitContents(elClickedObj) {
oEditors.getById["ir1"].exec("UPDATE_CONTENTS_FIELD", []); // 에디터의 내용이 textarea에 적용됩니다.
// 에디터의 내용에 대한 값 검증은 이곳에서 document.getElementById("ir1").value를 이용해서 처리하면 됩니다.
try {
elClickedObj.form.submit();
} catch(e) {}
}
function setDefaultFont() {
var sDefaultFont = '궁서';
var nFontSize = 24;
oEditors.getById["ir1"].setDefaultFont(sDefaultFont, nFontSize);
}
</script>
</body>
</html>

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,175 +0,0 @@
@charset "UTF-8";
/* NHN Web Standardization Team (http://html.nhndesign.com/) HHJ 090226 */
/* COMMON */
body,#smart_editor2,#smart_editor2 p,#smart_editor2 h1,#smart_editor2 h2,#smart_editor2 h3,#smart_editor2 h4,#smart_editor2 h5,#smart_editor2 h6,#smart_editor2 ul,#smart_editor2 ol,#smart_editor2 li,#smart_editor2 dl,#smart_editor2 dt,#smart_editor2 dd,#smart_editor2 table,#smart_editor2 th,#smart_editor2 td,#smart_editor2 form,#smart_editor2 fieldset,#smart_editor2 legend,#smart_editor2 input,#smart_editor2 textarea,#smart_editor2 button,#smart_editor2 select{margin:0;padding:0}
#smart_editor2,#smart_editor2 h1,#smart_editor2 h2,#smart_editor2 h3,#smart_editor2 h4,#smart_editor2 h5,#smart_editor2 h6,#smart_editor2 input,#smart_editor2 textarea,#smart_editor2 select,#smart_editor2 table,#smart_editor2 button{font-family:'돋움',Dotum,Helvetica,sans-serif;font-size:12px;color:#666}
#smart_editor2 span,#smart_editor2 em{font-size:12px}
#smart_editor2 em,#smart_editor2 address{font-style:normal}
#smart_editor2 img,#smart_editor2 fieldset{border:0}
#smart_editor2 hr{display:none}
#smart_editor2 ol,#smart_editor2 ul{list-style:none}
#smart_editor2 button{border:0;background:none;font-size:11px;vertical-align:top;cursor:pointer}
#smart_editor2 button span,#smart_editor2 button em{visibility:hidden;overflow:hidden;position:absolute;top:0;font-size:0;line-height:0}
#smart_editor2 legend,#smart_editor2 .blind{visibility:hidden;overflow:hidden;position:absolute;width:0;height:0;font-size:0;line-height:0}
#smart_editor2 .input_ty1{height:14px;margin:0;padding:4px 2px 0 4px;border:1px solid #c7c7c7;font-size:11px;color:#666}
#smart_editor2 a:link,#smart_editor2 a:visited,#smart_editor2 a:active,#smart_editor2 a:focus{color:#666;text-decoration:none}
#smart_editor2 a:hover{color:#666;text-decoration:underline}
/* LAYOUT */
#smart_editor2 .se2_header{margin:10px 0 29px 0}
#smart_editor2 .se2_bi{float:left;width:93px;height:20px;margin:0;padding:0;background:url("../img/ko_KR/btn_set.png?130306") -343px -358px no-repeat;font-size:0;line-height:0;text-indent:-10000px;vertical-align:middle}
#smart_editor2 .se2_allhelp{display:inline-block;width:18px;height:18px;padding:0;background:url("../img/ko_KR/btn_set.png?130306") -437px -358px no-repeat;font-size:0;line-height:0;text-indent:-10000px;vertical-align:middle}
#smart_editor2 #smart_editor2_content{border:1px solid #b5b5b5}
#smart_editor2 .se2_tool{overflow:visible;position:relative;z-index:25}
/* EDITINGAREA */
#smart_editor2 .se2_input_area{position:relative;z-index:22;height:400px;margin:0;padding:0;*zoom:1}
#smart_editor2 .se2_input_wysiwyg,#smart_editor2 .se2_input_syntax{display:block;overflow:auto;width:100%;height:100%;margin:0;*margin:-1px 0 0 0;border:0}
/* EDITINGMODE */
#smart_editor2 .se2_conversion_mode{position:relative;height:15px;padding-top:1px;border-top:1px solid #b5b5b5;background:url("../img/icon_set.gif") 0 -896px repeat-x}
#smart_editor2 .se2_inputarea_controller{display:block;clear:both;position:relative;width:100%;height:15px;text-align:center;cursor:n-resize}
#smart_editor2 .se2_inputarea_controller span,#smart_editor2 .controller_on span{background:url("../img/ico_extend.png") no-repeat}
#smart_editor2 .se2_inputarea_controller span{position:static;display:inline-block;visibility:visible;overflow:hidden;height:15px;padding-left:11px;background-position:0 2px;color:#888;font-size:11px;letter-spacing:-1px;line-height:16px;white-space:nowrap}
* + html #smart_editor2 .se2_inputarea_controller span{line-height:14px}
#smart_editor2 .controller_on span{background-position:0 -21px;color:#249c04}
#smart_editor2 .ly_controller{display:block;position:absolute;bottom:2px;left:50%;width:287px;margin-left:-148px;padding:8px 0 7px 9px;border:1px solid #827f7c;background:#fffdef}
#smart_editor2 .ly_controller p{color:#666;font-size:11px;letter-spacing:-1px;line-height:11px}
#smart_editor2 .ly_controller .bt_clse,#smart_editor2 .ly_controller .ic_arr{position:absolute;background:url("../img/ico_extend.png") no-repeat}
#smart_editor2 .ly_controller .bt_clse{top:5px;right:4px;width:14px;height:15px;background-position:1px -43px}
#smart_editor2 .ly_controller .ic_arr{top:25px;left:50%;width:10px;height:6px;margin-left:-5px;background-position:0 -65px}
#smart_editor2 .se2_converter{float:left;position:absolute;top:-1px;right:3px;z-index:20}
#smart_editor2 .se2_converter li{float:left}
#smart_editor2 .se2_converter .se2_to_editor{width:59px;height:15px;background:url("../img/ko_KR/btn_set.png?130306") 0 -85px no-repeat;vertical-align:top}
#smart_editor2 .se2_converter .se2_to_html{width:59px;height:15px;background:url("../img/ko_KR/btn_set.png?130306") -59px -70px no-repeat;vertical-align:top}
#smart_editor2 .se2_converter .se2_to_text{width:60px;height:15px;background:url("../img/ko_KR/btn_set.png?130306") -417px -466px no-repeat;vertical-align:top}
#smart_editor2 .se2_converter .active .se2_to_editor{width:59px;height:15px;background:url("../img/ko_KR/btn_set.png?130306") 0 -70px no-repeat;vertical-align:top}
#smart_editor2 .se2_converter .active .se2_to_html{width:59px;height:15px;background:url("../img/ko_KR/btn_set.png?130306") -59px -85px no-repeat;vertical-align:top}
#smart_editor2 .se2_converter .active .se2_to_text{width:60px;height:15px;background:url("../img/ko_KR/btn_set.png?130306") -417px -481px no-repeat;vertical-align:top}
/* EDITINGAREA_HTMLSRC */
#smart_editor2 .off .ico_btn,#smart_editor2 .off .se2_more,#smart_editor2 .off .se2_more2,#smart_editor2 .off .se2_font_family,#smart_editor2 .off .se2_font_size,#smart_editor2 .off .se2_bold,#smart_editor2 .off .se2_underline,#smart_editor2 .off .se2_italic,#smart_editor2 .off .se2_tdel,#smart_editor2 .off .se2_fcolor,#smart_editor2 .off .se2_fcolor_more,#smart_editor2 .off .se2_bgcolor,#smart_editor2 .off .se2_bgcolor_more,#smart_editor2 .off .se2_left,#smart_editor2 .off .se2_center,#smart_editor2 .off .se2_right,#smart_editor2 .off .se2_justify,#smart_editor2 .off .se2_ol,#smart_editor2 .off .se2_ul,#smart_editor2 .off .se2_indent,#smart_editor2 .off .se2_outdent,#smart_editor2 .off .se2_lineheight,#smart_editor2 .off .se2_del_style,#smart_editor2 .off .se2_blockquote,#smart_editor2 .off .se2_summary,#smart_editor2 .off .se2_footnote,#smart_editor2 .off .se2_url,#smart_editor2 .off .se2_emoticon,#smart_editor2 .off .se2_character,#smart_editor2 .off .se2_table,#smart_editor2 .off .se2_find,#smart_editor2 .off .se2_spelling,#smart_editor2 .off .se2_sup,#smart_editor2 .off .se2_sub,#smart_editor2 .off .se2_text_tool_more,#smart_editor2 .off .se2_new,#smart_editor2 .off .selected_color,#smart_editor2 .off .se2_lineSticker{-ms-filter:alpha(opacity=50);opacity:.5;cursor:default;filter:alpha(opacity=50)}
/* LAYER */
#smart_editor2 .se2_text_tool .se2_layer{display:none;float:left;position:absolute;top:20px;left:0;z-index:50;margin:0;padding:0;border:1px solid #bcbbbb;background:#fafafa}
#smart_editor2 .se2_text_tool li.active{z-index:50}
#smart_editor2 .se2_text_tool .active .se2_layer{display:block}
#smart_editor2 .se2_text_tool .active li .se2_layer{display:none}
#smart_editor2 .se2_text_tool .active .active .se2_layer{display:block}
#smart_editor2 .se2_text_tool .se2_layer .se2_in_layer{float:left;margin:0;padding:0;border:1px solid #fff;background:#fafafa}
/* TEXT_TOOLBAR */
#smart_editor2 .se2_text_tool{position:relative;clear:both;z-index:30;padding:4px 0 4px 3px;background:#f4f4f4 url("../img/bg_text_tool.gif") 0 0 repeat-x;border-bottom:1px solid #b5b5b5;*zoom:1}
#smart_editor2 .se2_text_tool:after{content:"";display:block;clear:both}
#smart_editor2 .se2_text_tool ul{float:left;display:inline;margin-right:3px;padding-left:1px;white-space:nowrap}
#smart_editor2 .se2_text_tool li{_display:inline;float:left;position:relative;z-index:30}
#smart_editor2 .se2_text_tool button,#smart_editor2 .se2_multy .se2_icon{width:21px;height:21px;background:url("../img/ko_KR/text_tool_set.png?140317") no-repeat;vertical-align:top}
#smart_editor2 .se2_text_tool .se2_font_type{position:relative}
#smart_editor2 .se2_text_tool .se2_font_type li{margin-left:3px}
#smart_editor2 .se2_text_tool .se2_font_type button{text-align:left}
#smart_editor2 .se2_text_tool .se2_font_type button.se2_font_family span,#smart_editor2 .se2_text_tool .se2_font_type button.se2_font_size span{display:inline-block;visibility:visible;position:static;width:52px;height:20px;padding:0 0 0 6px;font-size:12px;line-height:20px;*line-height:22px;color:#333;*zoom:1}
#smart_editor2 .se2_text_tool .se2_multy{position:absolute;top:0;right:0;padding-left:0;margin-right:0;white-space:nowrap;border-left:1px solid #e0dedf}
#smart_editor2 .se2_text_tool .se2_multy .se2_mn{float:left;white-space:nowrap}
#smart_editor2 .se2_text_tool .se2_multy button{background-image:none;width:47px}
#smart_editor2 .se2_text_tool .se2_multy .se2_icon{display:inline-block;visibility:visible;overflow:visible;position:static;width:16px;height:29px;margin:-1px 2px 0 -1px;background-position:0 -132px;line-height:30px;vertical-align:top}
#smart_editor2 .se2_text_tool .se2_multy button,#smart_editor2 .se2_text_tool .se2_multy button span{height:29px;line-height:29px}
#smart_editor2 .se2_text_tool .se2_map .se2_icon{background-position:-29px -132px}
#smart_editor2 .se2_text_tool button span.se2_mntxt{display:inline-block;visibility:visible;overflow:visible;_overflow-y:hidden;position:relative;*margin-right:-1px;width:auto;height:29px;font-weight:normal;font-size:11px;line-height:30px;*line-height:29px;_line-height:30px;color:#444;letter-spacing:-1px;vertical-align:top}
#smart_editor2 .se2_text_tool .se2_multy .se2_photo{margin-right:1px}
#smart_editor2 .se2_text_tool .se2_multy .hover .ico_btn{background:#e8e8e8}
#smart_editor2 .se2_text_tool .se2_multy .se2_mn.hover{background:#e0dedf}
/* TEXT_TOOLBAR : ROUNDING */
#smart_editor2 ul li.first_child button span.tool_bg,#smart_editor2 ul li.last_child button span.tool_bg,#smart_editor2 ul li.single_child button span.tool_bg{visibility:visible;height:21px}
#smart_editor2 ul li.first_child button span.tool_bg{left:-1px;width:3px;background:url("../img/bg_button_left.gif?20121228") no-repeat}
#smart_editor2 ul li.last_child button span.tool_bg{right:0px;_right:-1px;width:2px;background:url("../img/bg_button_right.gif") no-repeat}
#smart_editor2 ul li.single_child{padding-right:1px}
#smart_editor2 ul li.single_child button span.tool_bg{left:0;background:url("../img/bg_button.gif?20121228") no-repeat;width:22px}
#smart_editor2 div.se2_text_tool ul li.hover button span.tool_bg{background-position:0 -21px}
#smart_editor2 div.se2_text_tool ul li.active button span.tool_bg,#smart_editor2 div.se2_text_tool ul li.active li.active button span.tool_bg{background-position:0 -42px}
#smart_editor2 div.se2_text_tool ul li.active li button span.tool_bg{background-position:0 0}
/* TEXT_TOOLBAR : SUB_MENU */
#smart_editor2 .se2_sub_text_tool{display:none;position:absolute;top:20px;left:0;z-index:40;width:auto;height:29px;padding:0 4px 0 0;border:1px solid #b5b5b5;border-top:1px solid #9a9a9a;background:#f4f4f4}
#smart_editor2 .active .se2_sub_text_tool{display:block}
#smart_editor2 .se2_sub_text_tool ul{float:left;height:25px;margin:0;padding:4px 0 0 4px}
/* TEXT_TOOLBAR : SUB_MENU_SIZE */
#smart_editor2 .se2_sub_step1{width:88px}
#smart_editor2 .se2_sub_step2{width:199px}
#smart_editor2 .se2_sub_step2_1{width:178px}
/* TEXT_TOOLBAR : BUTTON */
#smart_editor2 .se2_text_tool .se2_font_family{width:70px;height:21px;background-position:0 -10px}
#smart_editor2 .se2_text_tool .hover .se2_font_family{background-position:0 -72px}
#smart_editor2 .se2_text_tool .active .se2_font_family{background-position:0 -103px}
#smart_editor2 .se2_text_tool .se2_font_size{width:45px;height:21px;background-position:-70px -10px}
#smart_editor2 .se2_text_tool .hover .se2_font_size{background-position:-70px -72px}
#smart_editor2 .se2_text_tool .active .se2_font_size{background-position:-70px -103px}
#smart_editor2 .se2_text_tool .se2_bold{background-position:-115px -10px}
#smart_editor2 .se2_text_tool .hover .se2_bold{background-position:-115px -72px}
#smart_editor2 .se2_text_tool .active .se2_bold{background-position:-115px -103px}
#smart_editor2 .se2_text_tool .se2_underline{background-position:-136px -10px}
#smart_editor2 .se2_text_tool .hover .se2_underline{background-position:-136px -72px}
#smart_editor2 .se2_text_tool .active .se2_underline{background-position:-136px -103px}
#smart_editor2 .se2_text_tool .se2_italic{background-position:-157px -10px}
#smart_editor2 .se2_text_tool .hover .se2_italic{background-position:-157px -72px}
#smart_editor2 .se2_text_tool .active .se2_italic{background-position:-157px -103px}
#smart_editor2 .se2_text_tool .se2_tdel{background-position:-178px -10px}
#smart_editor2 .se2_text_tool .hover .se2_tdel{background-position:-178px -72px}
#smart_editor2 .se2_text_tool .active .se2_tdel{background-position:-178px -103px}
#smart_editor2 .se2_text_tool .se2_fcolor{position:relative;background-position:-199px -10px}
#smart_editor2 .se2_text_tool .hover .se2_fcolor{background-position:-199px -72px}
#smart_editor2 .se2_text_tool .active .se2_fcolor{background-position:-199px -103px}
#smart_editor2 .se2_text_tool .se2_fcolor_more{background-position:-220px -10px;width:10px}
#smart_editor2 .se2_text_tool .hover .se2_fcolor_more{background-position:-220px -72px}
#smart_editor2 .se2_text_tool .active .se2_fcolor_more{background-position:-220px -103px}
#smart_editor2 .se2_text_tool .selected_color{position:absolute;top:14px;left:5px;width:11px;height:3px;font-size:0}
#smart_editor2 .se2_text_tool .se2_ol,#smart_editor2 .se2_text_tool .active .se2_sub_text_tool .se2_ol{background-position:-345px -10px}
#smart_editor2 .se2_text_tool .se2_ul,#smart_editor2 .se2_text_tool .active .se2_sub_text_tool .se2_ul{background-position:-366px -10px}
#smart_editor2 .se2_text_tool .hover .se2_ol,#smart_editor2 .se2_text_tool .active .se2_sub_text_tool .hover .se2_ol{background-position:-345px -72px}
#smart_editor2 .se2_text_tool .hover .se2_ul,#smart_editor2 .se2_text_tool .active .se2_sub_text_tool .hover .se2_ul{background-position:-366px -72px}
#smart_editor2 .se2_text_tool .active .se2_ol,#smart_editor2 .se2_text_tool .active .active .se2_ol{background-position:-345px -103px}
#smart_editor2 .se2_text_tool .active .se2_ul,#smart_editor2 .se2_text_tool .active .active .se2_ul{background-position:-366px -103px}
#smart_editor2 .se2_text_tool .se2_indent,#smart_editor2 .se2_text_tool .active .se2_sub_text_tool .se2_indent{background-position:-408px -10px}
#smart_editor2 .se2_text_tool .se2_outdent,#smart_editor2 .se2_text_tool .active .se2_sub_text_tool .se2_outdent{background-position:-387px -10px}
#smart_editor2 .se2_text_tool .hover .se2_indent,#smart_editor2 .se2_text_tool .active .se2_sub_text_tool .hover .se2_indent{background-position:-408px -72px}
#smart_editor2 .se2_text_tool .hover .se2_outdent,#smart_editor2 .se2_text_tool .active .se2_sub_text_tool .hover .se2_outdent{background-position:-387px -72px}
#smart_editor2 .se2_text_tool .active .se2_indent,#smart_editor2 .se2_text_tool .active .active .se2_indent{background-position:-408px -103px}
#smart_editor2 .se2_text_tool .active .se2_outdent,#smart_editor2 .se2_text_tool .active .active .se2_outdent{background-position:-387px -103px}
#smart_editor2 .se2_text_tool .se2_lineheight{background-position:-429px -10px}
#smart_editor2 .se2_text_tool .hover .se2_lineheight{background-position:-429px -72px}
#smart_editor2 .se2_text_tool .active .se2_lineheight{background-position:-429px -103px}
#smart_editor2 .se2_text_tool .se2_url{background-position:-513px -10px}
#smart_editor2 .se2_text_tool .hover .se2_url{background-position:-513px -72px}
#smart_editor2 .se2_text_tool .active .se2_url{background-position:-513px -103px}
#smart_editor2 .se2_text_tool .se2_bgcolor{position:relative;background-position:-230px -10px}
#smart_editor2 .se2_text_tool .hover .se2_bgcolor{background-position:-230px -72px}
#smart_editor2 .se2_text_tool .active .se2_bgcolor{background-position:-230px -103px}
#smart_editor2 .se2_text_tool .se2_bgcolor_more{background-position:-251px -10px;width:10px}
#smart_editor2 .se2_text_tool .hover .se2_bgcolor_more{background-position:-251px -72px}
#smart_editor2 .se2_text_tool .active .se2_bgcolor_more{background-position:-251px -103px}
#smart_editor2 .se2_text_tool .se2_left{background-position:-261px -10px}
#smart_editor2 .se2_text_tool .hover .se2_left{background-position:-261px -72px}
#smart_editor2 .se2_text_tool .active .se2_left{background-position:-261px -103px}
#smart_editor2 .se2_text_tool .se2_center{background-position:-282px -10px}
#smart_editor2 .se2_text_tool .hover .se2_center{background-position:-282px -72px}
#smart_editor2 .se2_text_tool .active .se2_center{background-position:-282px -103px}
#smart_editor2 .se2_text_tool .se2_right{background-position:-303px -10px}
#smart_editor2 .se2_text_tool .hover .se2_right{background-position:-303px -72px}
#smart_editor2 .se2_text_tool .active .se2_right{background-position:-303px -103px}
#smart_editor2 .se2_text_tool .se2_justify{background-position:-324px -10px}
#smart_editor2 .se2_text_tool .hover .se2_justify{background-position:-324px -72px}
#smart_editor2 .se2_text_tool .active .se2_justify{background-position:-324px -103px}
#smart_editor2 .se2_text_tool .se2_blockquote{background-position:-471px -10px}
#smart_editor2 .se2_text_tool .hover .se2_blockquote{background-position:-471px -72px}
#smart_editor2 .se2_text_tool .active .se2_blockquote{background-position:-471px -103px}
#smart_editor2 .se2_text_tool .se2_character{background-position:-555px -10px}
#smart_editor2 .se2_text_tool .hover .se2_character{background-position:-555px -72px}
#smart_editor2 .se2_text_tool .active .se2_character{background-position:-555px -103px}
#smart_editor2 .se2_text_tool .se2_table{background-position:-576px -10px}
#smart_editor2 .se2_text_tool .hover .se2_table{background-position:-576px -72px}
#smart_editor2 .se2_text_tool .active .se2_table{background-position:-576px -103px}
#smart_editor2 .se2_text_tool .se2_find{background-position:-597px -10px}
#smart_editor2 .se2_text_tool .hover .se2_find{background-position:-597px -72px}
#smart_editor2 .se2_text_tool .active .se2_find{background-position:-597px -103px}
#smart_editor2 .se2_text_tool .se2_sup{background-position:-660px -10px}
#smart_editor2 .se2_text_tool .hover .se2_sup{background-position:-660px -72px}
#smart_editor2 .se2_text_tool .active .se2_sup{background-position:-660px -103px}
#smart_editor2 .se2_text_tool .se2_sub{background-position:-681px -10px}
#smart_editor2 .se2_text_tool .hover .se2_sub{background-position:-681px -72px}
#smart_editor2 .se2_text_tool .active .se2_sub{background-position:-681px -103px}
#smart_editor2 .se2_text_tool .se2_text_tool_more{background-position:0 -41px;width:13px}
#smart_editor2 .se2_text_tool .se2_text_tool_more span.tool_bg{background:none}
#smart_editor2 .se2_text_tool .hover .se2_text_tool_more{background-position:-13px -41px}
#smart_editor2 .se2_text_tool .active .se2_text_tool_more{background-position:-26px -41px}

View File

@ -1,21 +0,0 @@
@charset "UTF-8";
/* NHN Web Standardization Team (http://html.nhndesign.com/) HHJ 090226 */
/* COMMON */
body,.se2_inputarea{margin:0;padding:0;font-family:'돋움',Dotum,Helvetica,Sans-serif;font-size:12px;line-height:1.5}
/* body,.se2_inputarea,.se2_inputarea th,.se2_inputarea td{margin:0;padding:0;font-family:'돋움',Dotum,Helvetica,Sans-serif;font-size:12px;line-height:1.5;color:#666} */
.se2_inputarea p,.se2_inputarea br{margin:0;padding:0}
.se2_inputarea{margin:15px;word-wrap:break-word;*word-wrap:normal;*word-break:break-all}
.se2_inputarea td{word-break:break-all}
.se2_inputarea_890{width:741px;margin:20px 0 10px 64px}
.se2_inputarea_698{width:548px;margin:20px 0 10px 64px}
/* TEXT_TOOLBAR : QUOTE */
.se2_quote1{margin:0 0 30px 20px;padding:0 8px;border-left:2px solid #ccc;color:#888}
.se2_quote2{margin:0 0 30px 13px;padding:0 8px 0 16px;background:url("../img/bg_quote2.gif") 0 3px no-repeat;color:#888}
.se2_quote3{margin:0 0 30px;padding:12px 10px 11px;border:1px dashed #ccc;color:#888}
.se2_quote4{margin:0 0 30px;padding:12px 10px 11px;border:1px dashed #66b246;color:#888}
.se2_quote5{margin:0 0 30px;padding:12px 10px 11px;border:1px dashed #ccc;background:#fafafa;color:#888}
.se2_quote6{margin:0 0 30px;padding:12px 10px 11px;border:1px solid #e5e5e5;color:#888}
.se2_quote7{margin:0 0 30px;padding:12px 10px 11px;border:1px solid #66b246;color:#888}
.se2_quote8{margin:0 0 30px;padding:12px 10px 11px;border:1px solid #e5e5e5;background:#fafafa;color:#888}
.se2_quote9{margin:0 0 30px;padding:12px 10px 11px;border:2px solid #e5e5e5;color:#888}
.se2_quote10{margin:0 0 30px;padding:12px 10px 11px;border:2px solid #e5e5e5;background:#fafafa;color:#888}

View File

@ -1,462 +0,0 @@
@charset "UTF-8";
/* NHN Web Standardization Team (http://html.nhndesign.com/) HHJ 090226 */
/* TEXT_TOOLBAR : FONTNAME */
#smart_editor2 .se2_tool .se2_l_font_fam{width:202px;margin:0;padding:0}
#smart_editor2 .se2_tool .se2_l_font_fam li{display:block;width:202px;height:21px;margin:0;padding:0;color:#333;cursor:pointer}
#smart_editor2 .se2_l_font_fam .hover,#smart_editor2 .se2_l_font_fam .active{background:#ebebeb}
#smart_editor2 .se2_l_font_fam button{width:200px;height:21px;margin:0;padding:2px 0 2px 0px;background:none;text-align:left}
#smart_editor2 .se2_l_font_fam button span{display:block;visibility:visible;overflow:visible;position:relative;top:auto;left:auto;width:auto;height:auto;margin:0 0 0 4px;padding:0;font-size:12px;line-height:normal;color:#333}
#smart_editor2 .se2_l_font_fam button span span{display:inline;visibility:visible;overflow:visible;width:auto;height:auto;margin:0 0 0 4px;font-family:Verdana;font-size:12px;line-height:14px;color:#888}
#smart_editor2 .se2_l_font_fam button span em{visibility:visible;overflow:auto;position:static;width:auto;height:auto;margin-right:-4px;font-size:12px;color:#888}
#smart_editor2 .se2_l_font_fam .se2_division{width:162px;height:2px !important;margin:1px 0 1px 0px;border:0;background:url("../img/bg_line1.gif") 0 0 repeat-x;font-size:0;cursor:default}
/* TEXT_TOOLBAR : FONTSIZE */
#smart_editor2 .se2_tool .se2_l_font_size{width:302px;margin:0;padding:0}
#smart_editor2 .se2_tool .se2_l_font_size li{width:302px;margin:0;padding:0;color:#333;cursor:pointer}
#smart_editor2 .se2_l_font_size .hover,#smart_editor2 .se2_l_font_size .active{background:#ebebeb}
#smart_editor2 .se2_l_font_size button{width:300px;height:auto;margin:0;padding:2px 0 1px 0px;*padding:4px 0 1px 0px;background:none;text-align:left}
#smart_editor2 .se2_l_font_size button span{display:block;visibility:visible;overflow:visible;position:relative;top:auto;left:auto;width:auto;height:auto;margin:0 0 0 4px;padding:0;line-height:normal;color:#373737;letter-spacing:0px}
#smart_editor2 .se2_l_font_size button span span{display:inline;margin:0 0 0 5px;padding:0}
#smart_editor2 .se2_l_font_size span em{visibility:visible;overflow:auto;position:static;width:auto;height:auto;color:#888}
/* TEXT_TOOLBAR : FONTCOLOR */
#smart_editor2 .se2_palette{float:left;position:relative;width:225px;margin:0;padding:11px 0 10px 0}
#smart_editor2 .se2_palette .se2_pick_color{_display:inline;float:left;clear:both;width:205px;margin:0 0 0 11px;padding:0}
#smart_editor2 .se2_palette .se2_pick_color li{float:left;width:12px;height:12px;margin:0;padding:0}
#smart_editor2 .se2_palette .se2_pick_color li button{width:11px;height:11px;border:0}
#smart_editor2 .se2_palette .se2_pick_color li button span{display:block;visibility:visible;overflow:visible;position:absolute;top:1px;left:1px;width:11px;height:11px}
#smart_editor2 .se2_palette .se2_pick_color li button span span{visibility:hidden;overflow:hidden;position:absolute;top:0;left:0;width:0;height:0}
#smart_editor2 .se2_palette .se2_pick_color .hover button,#smart_editor2 .se2_palette .se2_pick_color .active button{width:11px;height:11px;border:1px solid #666}
#smart_editor2 .se2_palette .se2_pick_color .hover span,#smart_editor2 .se2_palette .se2_pick_color .active span{width:7px;height:7px;border:1px solid #fff}
#smart_editor2 .se2_palette .se2_view_more{_display:inline;float:left;width:46px;height:23px;margin:1px 0 0 1px;background:url("../img/ko_KR/btn_set.png?130306") 0 -47px no-repeat}
#smart_editor2 .se2_palette .se2_view_more2{_display:inline;float:left;width:46px;height:23px;margin:1px 0 0 1px;background:url("../img/ko_KR/btn_set.png?130306") 0 -24px no-repeat}
#smart_editor2 .se2_palette h4{_display:inline;float:left;width:203px;margin:9px 0 0 11px;padding:10px 0 4px 0;background:url("../img/bg_line1.gif") repeat-x;font-weight:normal;font-size:12px;line-height:14px;color:#333;letter-spacing:-1px}
#smart_editor2 .se2_palette2{float:left;_float:none;width:214px;margin:9px 0 0 0;padding:11px 0 0 11px;background:url("../img/bg_line1.gif") repeat-x}
#smart_editor2 .se2_palette2 .se2_color_set{float:left}
#smart_editor2 .se2_palette2 .se2_selected_color{_display:inline;float:left;width:83px;height:18px;margin:0;border:1px solid #c7c7c7;background:#fff}
#smart_editor2 .se2_palette2 .se2_selected_color span{_display:inline;float:left;width:79px;height:14px;margin:2px}
#smart_editor2 .se2_palette2 .input_ty1{_display:inline;float:left;width:67px;height:16px;margin:0 3px 0 3px;padding:2px 2px 0 4px;font-family:tahoma;font-size:11px}
#smart_editor2 .se2_palette2 button.se2_btn_insert{float:left;width:35px;height:21px;margin-left:2px;padding:0;background:url("../img/ko_KR/btn_set.png?130306") -80px 0 no-repeat}
#smart_editor2 .se2_gradation1{float:left;_float:none;width:201px;height:128px;margin:4px 0 0 0;border:1px solid #c7c7c7;cursor:crosshair}
#smart_editor2 .se2_gradation2{float:left;_float:none;width:201px;height:10px;margin:4px 0 1px 0;border:1px solid #c7c7c7;cursor:crosshair}
/* TEXT_TOOLBAR : BGCOLOR */
#smart_editor2 .se2_palette_bgcolor{width:225px;margin:11px 0 0;padding:0}
#smart_editor2 .se2_palette_bgcolor .se2_background{width:205px;margin:0 11px 0 11px}
#smart_editor2 .se2_palette_bgcolor .se2_background li{width:68px;height:20px}
#smart_editor2 .se2_palette_bgcolor .se2_background button{width:67px;height:19px;border:0}
#smart_editor2 .se2_palette_bgcolor .se2_background span{left:0;display:block;visibility:visible;overflow:visible;width:65px;height:17px;padding:0}
#smart_editor2 .se2_palette_bgcolor .se2_background span span{display:block;visibility:visible;overflow:visible;width:64px;height:16px;padding:3px 0 0 3px;font-size:11px;line-height:14px;text-align:left}
#smart_editor2 .se2_palette_bgcolor .se2_background .hover span{width:65px;height:17px;border:1px solid #666}
#smart_editor2 .se2_palette_bgcolor .se2_background .hover span span{width:62px;height:14px;padding:1px 0 0 1px;border:1px solid #fff}
/* TEXT_TOOLBAR : LINEHEIGHT */
#smart_editor2 .se2_l_line_height{width:107px;margin:0;padding:0}
#smart_editor2 .se2_l_line_height li{width:107px;margin:0;padding:0;border-top:0;border-bottom:0;color:#333;cursor:pointer}
#smart_editor2 .se2_l_line_height .hover{background:#ebebeb}
#smart_editor2 .se2_l_line_height button{width:105px;height:19px;margin:0;padding:3px 0 2px 0px;background:none;text-align:left}
#smart_editor2 .se2_l_line_height button span{visibility:visible;overflow:visible;position:relative;width:auto;height:auto;margin:0;padding:0 0 0 15px;font-size:12px;line-height:normal;color:#373737}
#smart_editor2 .se2_l_line_height li button.active span{background:url("../img/icon_set.gif") 5px -30px no-repeat}
#smart_editor2 .se2_l_line_height_user{clear:both;width:83px;margin:5px 0 0 12px;padding:10px 0 0 0;_padding:11px 0 0 0;background:url("../img/bg_line1.gif") repeat-x}
#smart_editor2 .se2_l_line_height_user h3{margin:0 0 4px 0;_margin:0 0 2px -1px;padding:0;line-height:14px;color:#000;letter-spacing:-1px}
#smart_editor2 .se2_l_line_height_user .bx_input{display:block;position:relative;width:83px}
#smart_editor2 .se2_l_line_height_user .btn_up{position:absolute;top:2px;*top:3px;left:68px;width:13px;height:8px;background:url("../img/ko_KR/btn_set.png?130306") -86px -54px no-repeat}
#smart_editor2 .se2_l_line_height_user .btn_down{position:absolute;top:10px;*top:11px;left:68px;width:13px;height:8px;background:url("../img/ko_KR/btn_set.png?130306") -86px -62px no-repeat}
#smart_editor2 .se2_l_line_height_user .btn_area{margin:5px 0 10px 0}
#smart_editor2 .se2_tool .btn_area .se2_btn_apply3{width:41px;height:24px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat}
#smart_editor2 .se2_tool .btn_area .se2_btn_cancel3{width:39px;height:24px;margin-left:3px;background:url("../img/ko_KR/btn_set.png?130306") -41px 0 no-repeat}
/* TEXT_TOOLBAR : QUOTE */
#smart_editor2 .se2_quote{width:425px;height:56px}
#smart_editor2 .se2_quote ul{_display:inline;float:left;margin:11px 0 0 9px;padding:0}
#smart_editor2 .se2_quote li{_display:inline;float:left;margin:0 0 0 2px;padding:0}
#smart_editor2 .se2_quote button{width:34px;height:34px;margin:0;padding:0;background:url("../img/ko_KR/btn_set.png?130306") no-repeat;cursor:pointer}
#smart_editor2 .se2_quote button span{left:0;display:block;visibility:visible;overflow:visible;width:32px;height:32px;margin:0;padding:0;border:1px solid #c7c7c7}
#smart_editor2 .se2_quote button span span{visibility:hidden;overflow:hidden;position:absolute;top:0;left:0;width:0;height:0;margin:0;padding:0}
#smart_editor2 .se2_quote .se2_quote1{background-position:1px -375px}
#smart_editor2 .se2_quote .se2_quote2{background-position:-32px -375px}
#smart_editor2 .se2_quote .se2_quote3{background-position:-65px -375px}
#smart_editor2 .se2_quote .se2_quote4{background-position:-98px -375px}
#smart_editor2 .se2_quote .se2_quote5{background-position:-131px -375px}
#smart_editor2 .se2_quote .se2_quote6{background-position:-164px -375px}
#smart_editor2 .se2_quote .se2_quote7{background-position:-197px -375px}
#smart_editor2 .se2_quote .se2_quote8{background-position:-230px -375px}
#smart_editor2 .se2_quote .se2_quote9{background-position:-263px -375px}
#smart_editor2 .se2_quote .se2_quote10{background-position:-296px -375px}
#smart_editor2 .se2_quote .hover button span,#smart_editor2 .se2_quote .active button span{width:30px;height:30px;margin:0;padding:0;border:2px solid #44b525}
#smart_editor2 .se2_quote .hover button span span,#smart_editor2 .se2_quote .active button span span{visibility:hidden;overflow:hidden;position:absolute;top:0;left:0;width:0;height:0;margin:0;padding:0}
#smart_editor2 .se2_quote .se2_cancel2{float:left;width:40px;height:35px;margin:11px 0 0 5px;background:url("../img/ko_KR/btn_set.png?130306") -46px -24px no-repeat}
#smart_editor2 .se2_quote .se2_cancel2 span{visibility:hidden;overflow:hidden;position:absolute;top:0;left:0;width:0;height:0;margin:0;padding:0}
/* TEXT_TOOLBAR : HYPERLINK */
#smart_editor2 .se2_url2{width:281px;padding:11px 11px 6px 11px;color:#666}
#smart_editor2 .se2_url2 .input_ty1{display:block;width:185px;height:16px;margin:0 5px 5px 0;*margin:-1px 5px 5px 0;padding:5px 2px 0 4px}
#smart_editor2 .se2_url2 .se2_url_new{width:15px;height:15px;margin:-1px 3px 1px -1px;*margin:-2px 3px 2px -1px;vertical-align:middle}
#smart_editor2 .se2_url2 label{font-size:11px;line-height:14px;vertical-align:middle}
#smart_editor2 .se2_url2 .se2_apply{position:absolute;top:13px;right:51px;width:41px;height:24px;margin:-1px 3px 1px 0;background:url("../img/ko_KR/btn_set.png?130306") no-repeat}
#smart_editor2 .se2_url2 .se2_cancel{position:absolute;top:13px;right:9px;width:39px;height:24px;margin:-1px 3px 1px 0;background:url("../img/ko_KR/btn_set.png?130306") -41px 0 no-repeat}
/* TEXT_TOOLBAR : SCHARACTER */
#smart_editor2 .se2_bx_character{width:469px;height:272px;margin:0;padding:0;background:url("../img/ko_KR/bx_set_110302.gif") 9px -1230px no-repeat}
#smart_editor2 .se2_bx_character .se2_char_tab{_display:inline;float:left;position:relative;width:443px;margin:11px 10px 200px 11px;padding:0 0 0 1px}
#smart_editor2 .se2_bx_character .se2_char_tab li{position:static;margin:0 0 0 -1px;padding:0}
#smart_editor2 .se2_bx_character .se2_char1{width:76px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") 0 -204px no-repeat}
#smart_editor2 .se2_bx_character .se2_char2{width:86px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -75px -204px no-repeat}
#smart_editor2 .se2_bx_character .se2_char3{width:68px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -160px -204px no-repeat}
#smart_editor2 .se2_bx_character .se2_char4{width:55px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -227px -204px no-repeat}
#smart_editor2 .se2_bx_character .se2_char5{width:97px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -281px -204px no-repeat}
#smart_editor2 .se2_bx_character .se2_char6{width:66px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -377px -204px no-repeat}
#smart_editor2 .se2_bx_character .active .se2_char1{width:76px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") 0 -230px no-repeat}
#smart_editor2 .se2_bx_character .active .se2_char2{width:86px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -75px -230px no-repeat}
#smart_editor2 .se2_bx_character .active .se2_char3{width:68px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -160px -230px no-repeat}
#smart_editor2 .se2_bx_character .active .se2_char4{width:55px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -227px -230px no-repeat}
#smart_editor2 .se2_bx_character .active .se2_char5{width:97px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -281px -230px no-repeat}
#smart_editor2 .se2_bx_character .active .se2_char6{width:66px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -377px -230px no-repeat}
#smart_editor2 .se2_bx_character .se2_s_character{display:none;position:absolute;top:26px;left:0;width:448px;height:194px;margin:0;padding:0}
#smart_editor2 .se2_bx_character .active .se2_s_character{display:block}
#smart_editor2 .se2_bx_character .se2_s_character ul{float:left;width:422px;height:172px;margin:0;padding:9px 0 0 11px}
#smart_editor2 .se2_bx_character .se2_s_character li{_display:inline;float:left;position:relative;width:20px;height:18px;margin:0 0 1px 1px;background:#fff}
#smart_editor2 .se2_bx_character .se2_s_character button{width:20px;height:18px;margin:0;padding:2px;background:none}
#smart_editor2 .se2_bx_character .se2_s_character .hover,#smart_editor2 .se2_bx_character .se2_s_character .active{background:url("../img/ko_KR/btn_set.png?130306") -446px -274px no-repeat}
#smart_editor2 .se2_bx_character .se2_s_character button span{left:0;display:block;visibility:visible;overflow:visible;width:14px;height:16px;margin:3px 0 0 3px;border:0;background:none;font-size:12px;line-height:normal}
#smart_editor2 .se2_apply_character{clear:both;position:relative;padding:0 0 0 11px}
#smart_editor2 .se2_apply_character label{margin:0 3px 0 0;font-size:12px;color:#666;letter-spacing:-1px}
#smart_editor2 .se2_apply_character .input_ty1{width:283px;height:17px;margin:-1px 5px 1px 0;padding:4px 0 0 5px;font-size:12px;color:#666;letter-spacing:0;vertical-align:middle}
#smart_editor2 .se2_apply_character .se2_confirm{width:41px;height:24px;margin-right:3px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat;vertical-align:middle}
#smart_editor2 .se2_apply_character .se2_cancel{width:39px;height:24px;background:url("../img/ko_KR/btn_set.png?130306") -41px 0 no-repeat;vertical-align:middle}
/* TEXT_TOOLBAR : TABLECREATOR */
#smart_editor2 .se2_table_set{position:relative;width:166px;margin:3px 11px 0 11px;padding:8px 0 0 0}
#smart_editor2 .se2_table_set .se2_cell_num{float:left;width:73px}
#smart_editor2 .se2_table_set .se2_cell_num dt{float:left;clear:both;width:17px;height:23px;margin:0;padding:0}
#smart_editor2 .se2_table_set .se2_cell_num dt label{display:block;margin:5px 0 0 0;font-size:11px;color:#666}
#smart_editor2 .se2_table_set .se2_cell_num dd{float:left;position:relative;width:54px;height:23px;margin:0;padding:0}
#smart_editor2 .se2_table_set .se2_cell_num .input_ty2{display:block;width:32px;height:16px;*margin:-1px 0 0 0;padding:2px 19px 0 0px;border:1px solid #c7c7c7;font-family:tahoma,verdana,times New Roman;font-size:11px;color:#666;text-align:right;*direction:rtl}
#smart_editor2 .se2_table_set .se2_cell_num .input_ty2::-ms-clear{display:none}
#smart_editor2 .se2_table_set .se2_pre_table{float:right;width:91px;height:43px;background:#c7c7c7;border-spacing:1px}
#smart_editor2 .se2_table_set .se2_pre_table tr{background:#fff}
#smart_editor2 .se2_table_set .se2_pre_table td{font-size:0;line-height:0}
#smart_editor2 .se2_table_set .se2_add{position:absolute;top:2px;right:3px;width:13px;height:8px;background:url("../img/ko_KR/btn_set.png?130306") -86px -54px no-repeat}
#smart_editor2 .se2_table_set .se2_del{position:absolute;top:10px;right:3px;width:13px;height:8px;background:url("../img/ko_KR/btn_set.png?130306") -86px -62px no-repeat}
/* TEXT_TOOLBAR : TABLEEDITOR */
#smart_editor2 .se2_table_set .se2_t_proper1{float:left;width:166px;margin:7px 0 0 0;padding:10px 0 5px;background:url("../img/bg_line1.gif") repeat-x}
#smart_editor2 .se2_table_set .se2_t_proper1 dt{width:166px;margin:0 0 6px 0}
#smart_editor2 .se2_table_set .se2_t_proper1 dd{width:166px}
#smart_editor2 .se2_table_set .se2_t_proper1 dt input{width:15px;height:15px;margin:-1px 3px 1px 0;_margin:-2px 3px 2px 0;vertical-align:middle}
#smart_editor2 .se2_table_set .se2_t_proper1 dt label{font-weight:bold;font-size:11px;color:#666;letter-spacing:-1px;vertical-align:middle}
#smart_editor2 .se2_table_set .se2_t_proper1_1{float:left;position:relative;z-index:59;width:166px;margin:1px 0 0 0}
#smart_editor2 .se2_table_set .se2_t_proper1_2{z-index:54;margin:0}
#smart_editor2 .se2_table_set .se2_t_proper1_3{z-index:53;margin:0}
#smart_editor2 .se2_table_set .se2_t_proper1_4{z-index:52;margin:0}
#smart_editor2 .se2_table_set .se2_t_proper1_1 dt{_display:inline;float:left;clear:both;width:66px;height:22px;margin:1px 0 0 18px}
#smart_editor2 .se2_table_set .se2_t_proper1_1 dt label{display:block;margin:4px 0 0 0;font-weight:normal;font-size:11px;color:#666;letter-spacing:-1px}
#smart_editor2 .se2_table_set .se2_t_proper1_1 dd{float:left;position:relative;width:82px;height:23px}
#smart_editor2 .se2_table_set .se2_t_proper1_1 .input_ty1{width:72px;height:16px;*margin:-1px 0 0 0;padding:2px 2px 0 6px;font-family:tahoma,verdana,times New Roman;font-size:11px;color:#666}
#smart_editor2 .se2_table_set .se2_t_proper1_1 .input_ty3{float:left;width:49px;height:16px;margin:0 3px 0 0;padding:2px 4px 0 4px;border:1px solid #c7c7c7;font-family:tahoma,verdana,times New Roman;font-size:11px;color:#666}
#smart_editor2 .se2_table_set .se2_t_proper1_1 .se2_add{top:2px;right:2px}
#smart_editor2 .se2_table_set .se2_t_proper1_1 .se2_del{top:10px;right:2px}
#smart_editor2 .se2_table_set .se2_t_proper1_1 .se2_color_set .input_ty1{_display:inline;float:left;width:67px;height:16px;margin:0 3px 0 3px;padding:2px 2px 0 4px;font-family:tahoma,verdana,times New Roman;font-size:11px}
#smart_editor2 .se2_select_ty1{position:relative;width:80px;height:18px;border:1px solid #c7c7c7;background:#fff;font-size:11px;line-height:14px;text-align:left}
#smart_editor2 .se2_select_ty1 span{float:left;width:54px;height:18px;margin:0 0 0 5px;font-size:11px;line-height:14px;color:#666}
#smart_editor2 .se2_select_ty1 .se2_b_style0{position:relative;top:3px;left:-3px;white-space:nowrap}
#smart_editor2 .se2_select_ty1 .se2_b_style1{height:15px;margin:3px 0 0 4px;font-size:11px;line-height:14px;color:#666;letter-spacing:-1px}
#smart_editor2 .se2_select_ty1 .se2_b_style2{background:url("../img/bg_set.gif") 0 -50px repeat-x}
#smart_editor2 .se2_select_ty1 .se2_b_style3{background:url("../img/bg_set.gif") 0 -68px repeat-x}
#smart_editor2 .se2_select_ty1 .se2_b_style4{background:url("../img/bg_set.gif") 0 -85px repeat-x}
#smart_editor2 .se2_select_ty1 .se2_b_style5{background:url("../img/bg_set.gif") 0 -103px repeat-x}
#smart_editor2 .se2_select_ty1 .se2_b_style6{background:url("../img/bg_set.gif") 0 -121px repeat-x}
#smart_editor2 .se2_select_ty1 .se2_b_style7{background:url("../img/bg_set.gif") 0 -139px repeat-x}
#smart_editor2 .se2_select_ty1 .se2_view_more{position:absolute;top:1px;right:1px;width:13px;height:16px;background:url("../img/ko_KR/btn_set.png?130306") -112px -54px no-repeat}
#smart_editor2 .se2_select_ty1 .se2_view_more2{position:absolute;top:1px;right:1px;width:13px;height:16px;background:url("../img/ko_KR/btn_set.png?130306") -99px -54px no-repeat}
/* TEXT_TOOLBAR : TABLEEDITOR > BORDER */
#smart_editor2 .se2_table_set .se2_b_t_b1{border-top:1px solid #b1b1b1}
#smart_editor2 .se2_layer_b_style{position:absolute;top:20px;right:0px;width:80px;padding-bottom:1px;border:1px solid #c7c7c7;border-top:1px solid #a8a8a8;background:#fff}
#smart_editor2 .se2_layer_b_style ul{width:80px;margin:0;padding:1px 0 0 0}
#smart_editor2 .se2_layer_b_style li{width:80px;height:18px;margin:0;padding:0}
#smart_editor2 .se2_layer_b_style .hover,#smart_editor2 .se2_layer_b_style .active{background:#ebebeb}
#smart_editor2 .se2_layer_b_style button{width:80px;height:18px;background:none}
#smart_editor2 .se2_layer_b_style button span{left:0;display:block;visibility:visible;overflow:visible;width:71px;height:18px;margin:0 0 0 5px;font-size:11px;line-height:15px;text-align:left}
#smart_editor2 .se2_layer_b_style button span span{visibility:hidden;overflow:hidden;position:absolute;top:0;left:0;width:0;height:0}
#smart_editor2 .se2_layer_b_style .se2_b_style1 span{margin:3px 0 0 4px;font-size:11px;line-height:14px;color:#666;letter-spacing:-1px}
#smart_editor2 .se2_layer_b_style .se2_b_style2 span{background:url("../img/bg_set.gif") 0 -50px repeat-x}
#smart_editor2 .se2_layer_b_style .se2_b_style3 span{background:url("../img/bg_set.gif") 0 -68px repeat-x}
#smart_editor2 .se2_layer_b_style .se2_b_style4 span{background:url("../img/bg_set.gif") 0 -86px repeat-x}
#smart_editor2 .se2_layer_b_style .se2_b_style5 span{background:url("../img/bg_set.gif") 0 -103px repeat-x}
#smart_editor2 .se2_layer_b_style .se2_b_style6 span{background:url("../img/bg_set.gif") 0 -121px repeat-x}
#smart_editor2 .se2_layer_b_style .se2_b_style7 span{background:url("../img/bg_set.gif") 0 -139px repeat-x}
/* TEXT_TOOLBAR : TABLEEDITOR > COLOR */
#smart_editor2 .se2_pre_color{float:left;width:18px;height:18px;border:1px solid #c7c7c7}
#smart_editor2 .se2_pre_color button{float:left;width:14px;height:14px;margin:2px 0 0 2px;padding:0}
#smart_editor2 .se2_pre_color button span{overflow:hidden;position:absolute;top:-10000px;left:-10000px;z-index:-100;width:0;height:0}
/* TEXT_TOOLBAR : TABLEEDITOR > DIMMED */
#smart_editor2 .se2_table_set .se2_t_dim1{clear:both;position:absolute;top:71px;left:16px;z-index:60;width:157px;height:118px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_table_set .se2_t_dim2{position:absolute;top:116px;left:16px;z-index:55;width:157px;height:45px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_table_set .se2_t_dim3{clear:both;position:absolute;top:192px;left:16px;z-index:51;width:157px;height:39px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
/* TEXT_TOOLBAR : TABLEEDITOR > STYLE PREVIEW */
#smart_editor2 .se2_table_set .se2_t_proper2{float:left;position:relative;z-index:50;width:166px;margin:2px 0 0 0}
#smart_editor2 .se2_table_set .se2_t_proper2 dt{float:left;width:84px;height:33px;margin:4px 0 0 0}
#smart_editor2 .se2_table_set .se2_t_proper2 dt input{width:15px;height:15px;margin:-1px 3px 1px 0;_margin:-2px 3px 2px 0;vertical-align:middle}
#smart_editor2 .se2_table_set .se2_t_proper2 dt label{font-weight:bold;font-size:11px;color:#666;letter-spacing:-1px;vertical-align:middle}
#smart_editor2 .se2_table_set .se2_t_proper2 dd{float:left;width:66px;height:33px}
#smart_editor2 .se2_select_ty2{position:relative;width:65px;height:31px;border:1px solid #c7c7c7;background:#fff;font-size:11px;line-height:14px;text-align:left}
#smart_editor2 .se2_select_ty2 span{float:left;width:45px;height:25px;margin:3px 0 0 3px;background:url("../img/ko_KR/btn_set.png?130306") repeat-x}
#smart_editor2 .se2_select_ty2 .se2_t_style1{background-position:0 -410px}
#smart_editor2 .se2_select_ty2 .se2_t_style2{background-position:-46px -410px}
#smart_editor2 .se2_select_ty2 .se2_t_style3{background-position:-92px -410px}
#smart_editor2 .se2_select_ty2 .se2_t_style4{background-position:-138px -410px}
#smart_editor2 .se2_select_ty2 .se2_t_style5{background-position:-184px -410px}
#smart_editor2 .se2_select_ty2 .se2_t_style6{background-position:-230px -410px}
#smart_editor2 .se2_select_ty2 .se2_t_style7{background-position:-276px -410px}
#smart_editor2 .se2_select_ty2 .se2_t_style8{background-position:-322px -410px}
#smart_editor2 .se2_select_ty2 .se2_t_style9{background-position:0 -436px}
#smart_editor2 .se2_select_ty2 .se2_t_style10{background-position:-46px -436px}
#smart_editor2 .se2_select_ty2 .se2_t_style11{background-position:-92px -436px}
#smart_editor2 .se2_select_ty2 .se2_t_style12{background-position:-138px -436px}
#smart_editor2 .se2_select_ty2 .se2_t_style13{background-position:-184px -436px}
#smart_editor2 .se2_select_ty2 .se2_t_style14{background-position:-230px -436px}
#smart_editor2 .se2_select_ty2 .se2_t_style15{background-position:-276px -436px}
#smart_editor2 .se2_select_ty2 .se2_t_style16{background-position:-322px -436px}
#smart_editor2 .se2_select_ty2 .se2_view_more{position:absolute;top:1px;right:1px;_right:0px;width:13px !important;height:29px !important;background:url("../img/ko_KR/btn_set.png?130306") -353px -48px no-repeat !important}
#smart_editor2 .se2_select_ty2 .se2_view_more2{position:absolute;top:1px;right:1px;_right:0px;width:13px !important;height:29px !important;background:url("../img/ko_KR/btn_set.png?130306") -340px -48px no-repeat !important}
#smart_editor2 .se2_select_ty2 .se2_view_more span{display:none}
/* TEXT_TOOLBAR : TABLEEDITOR > STYLE */
#smart_editor2 .se2_layer_t_style{position:absolute;top:33px;right:15px;width:208px;border:1px solid #c7c7c7;border-top:1px solid #a8a8a8;background:#fff}
#smart_editor2 .se2_layer_t_style ul{width:204px;height:126px;margin:1px 2px;padding:1px 0 0 0;background:#fff}
#smart_editor2 .se2_layer_t_style li{_display:inline;float:left;width:45px;height:25px;margin:1px;padding:1px;border:1px solid #fff}
#smart_editor2 .se2_layer_t_style .hover,#smart_editor2 .se2_layer_t_style .active{border:1px solid #666;background:#fff}
#smart_editor2 .se2_layer_t_style button{width:45px;height:25px;background:url("../img/ko_KR/btn_set.png?130306") repeat-x !important}
#smart_editor2 .se2_layer_t_style .se2_t_style1{background-position:0 -410px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style2{background-position:-46px -410px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style3{background-position:-92px -410px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style4{background-position:-138px -410px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style5{background-position:-184px -410px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style6{background-position:-230px -410px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style7{background-position:-276px -410px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style8{background-position:-322px -410px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style9{background-position:0 -436px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style10{background-position:-46px -436px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style11{background-position:-92px -436px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style12{background-position:-138px -436px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style13{background-position:-184px -436px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style14{background-position:-230px -436px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style15{background-position:-276px -436px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style16{background-position:-322px -436px !important}
#smart_editor2 .se2_table_set .se2_btn_area{float:left;width:166px;margin:6px 0 0 0;padding:12px 0 8px 0;background:url("../img/bg_line1.gif") repeat-x;text-align:center}
#smart_editor2 .se2_table_set button.se2_apply{width:41px;height:24px;margin-right:3px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat}
#smart_editor2 .se2_table_set button.se2_cancel{width:39px;height:24px;background:url("../img/ko_KR/btn_set.png?130306") -41px 0 no-repeat}
#smart_editor2 .se2_table_set .se2_rd{width:14px;height:14px;vertical-align:middle}
#smart_editor2 .se2_table_set .se2_celltit{font-size:11px;font-size:11px;color:#666;letter-spacing:-1px}
#smart_editor2 .se2_table_set dt label.se2_celltit{display:inline}
/* TEXT_TOOLBAR : FINDREPLACE */
#smart_editor2 .se2_bx_find_revise{position:relative;width:255px;margin:0;padding:0}
#smart_editor2 .se2_bx_find_revise .se2_close{position:absolute;top:5px;right:8px;width:20px;height:20px;background:url("../img/ko_KR/btn_set.png?130306") -151px -1px no-repeat}
#smart_editor2 .se2_bx_find_revise h3{margin:0;padding:10px 0 13px 10px;background:url("../img/bg_find_h3.gif") 0 -1px repeat-x;font-size:12px;line-height:14px;letter-spacing:-1px}
#smart_editor2 .se2_bx_find_revise ul{position:relative;margin:8px 0 0 0;padding:0 0 0 12px}
#smart_editor2 .se2_bx_find_revise ul li{_display:inline;float:left;position:static;margin:0 0 0 -1px;padding:0}
#smart_editor2 .se2_bx_find_revise .se2_tabfind{width:117px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") 0 -100px no-repeat}
#smart_editor2 .se2_bx_find_revise .se2_tabrevise{width:117px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -116px -100px no-repeat}
#smart_editor2 .se2_bx_find_revise .active .se2_tabfind{width:117px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") 0 -126px no-repeat}
#smart_editor2 .se2_bx_find_revise .active .se2_tabrevise{width:117px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -116px -126px no-repeat}
#smart_editor2 .se2_bx_find_revise .se2_in_bx_find dl{_display:inline;float:left;width:223px;margin:0 0 0 9px;padding:7px 0 13px 14px;background:url("../img/ko_KR/bx_set_110302.gif") -289px -1518px no-repeat}
#smart_editor2 .se2_bx_find_revise .se2_in_bx_revise dl{_display:inline;float:left;width:223px;margin:0 0 0 9px;padding:7px 0 13px 14px;background:url("../img/ko_KR/bx_set_110302.gif") -289px -1619px no-repeat}
#smart_editor2 .se2_bx_find_revise dt{_display:inline;float:left;clear:both;width:47px;margin:1px 0 2px 0}
#smart_editor2 .se2_bx_find_revise dd{float:left;margin:0 0 2px 0}
#smart_editor2 .se2_bx_find_revise label{float:left;padding:5px 0 0 0;font-size:11px;color:#666;letter-spacing:-2px}
#smart_editor2 .se2_bx_find_revise input{float:left;width:155px;height:12px;margin:1px 0 0 0;padding:3px 2px 3px 4px;font-size:12px;color:#666}
#smart_editor2 .se2_bx_find_revise .se2_find_btns{float:left;clear:both;width:255px;padding:8px 0 10px 0;text-align:center}
#smart_editor2 .se2_bx_find_revise .se2_find_next{width:65px;height:24px;margin:0 3px 0 0;background:url("../img/ko_KR/btn_set.png?130306") -180px -48px no-repeat}
#smart_editor2 .se2_bx_find_revise .se2_find_next2{width:61px;height:24px;margin:0 3px 0 0;background:url("../img/ko_KR/btn_set.png?130306") -180px -24px no-repeat}
#smart_editor2 .se2_bx_find_revise .se2_revise1{width:54px;height:24px;margin:0 3px 0 0;background:url("../img/ko_KR/btn_set.png?130306") -245px -48px no-repeat}
#smart_editor2 .se2_bx_find_revise .se2_revise2{width:70px;height:24px;margin:0 3px 0 0;background:url("../img/ko_KR/btn_set.png?130306") -245px -24px no-repeat}
#smart_editor2 .se2_bx_find_revise .se2_cancel{width:39px;height:24px;background:url("../img/ko_KR/btn_set.png?130306") -41px 0 no-repeat}
/* TEXT_TOOLBAR : QUICKEDITOR_TABLE */
#smart_editor2 .se2_qmax{position:absolute;width:18px;height:18px;background:url("../img/ko_KR/btn_set.png?130306") -339px -169px no-repeat}
#smart_editor2 .se2_qeditor{position:absolute;top:0;left:0;width:183px;margin:0;padding:0;border:1px solid #c7c7c7;border-right:1px solid #ababab;border-bottom:1px solid #ababab;background:#fafafa}
#smart_editor2 .se2_qeditor label,#smart_editor2 .se2_qeditor span,#smart_editor2 .se2_qeditor dt{font-size:11px;color:#666;letter-spacing:-1px}
#smart_editor2 .se2_qbar{position:relative;width:183px;height:11px;background:url("../img/ko_KR/bx_set_110302.gif") 0 -731px no-repeat}
#smart_editor2 .se2_qbar .se2_qmini{position:absolute;top:-1px;right:0;*right:-1px;_right:-3px;width:18px;height:14px;background:url("../img/ko_KR/btn_set.png?130306") -315px -170px no-repeat}
#smart_editor2 .se2_qbar .se2_qmini button{width:20px;height:14px;margin-top:-1px}
#smart_editor2 .se2_qeditor .se2_qbody0{float:left;border:1px solid #fefefe}
#smart_editor2 .se2_qeditor .se2_qbody{position:relative;z-index:90;width:174px;padding:4px 0 0 7px}
#smart_editor2 .se2_qeditor .se2_qe1{overflow:hidden;width:174px}
#smart_editor2 .se2_qeditor .se2_qe1 dt{float:left;width:22px;height:18px;padding:4px 0 0 0}
#smart_editor2 .se2_qeditor .se2_qe1 dd{float:left;width:65px;height:22px}
#smart_editor2 .se2_qeditor .se2_addrow{width:28px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -385px -49px}
#smart_editor2 .se2_qeditor .se2_addcol{width:29px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -413px -49px}
#smart_editor2 .se2_qeditor .se2_seprow{width:28px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -385px -68px}
#smart_editor2 .se2_qeditor .se2_sepcol{width:29px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -413px -68px}
#smart_editor2 .se2_qeditor .se2_delrow{width:28px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -385px -106px}
#smart_editor2 .se2_qeditor .se2_delcol{width:29px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -413px -106px}
#smart_editor2 .se2_qeditor .se2_merrow{width:57px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -385px -125px}
#smart_editor2 .se2_qeditor .se2_mercol{width:57px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -413px -125px}
#smart_editor2 .se2_qeditor .se2_seprow_off{width:28px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -385px -87px}
#smart_editor2 .se2_qeditor .se2_sepcol_off{width:29px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -413px -87px}
#smart_editor2 .se2_qeditor .se2_merrow_off{width:57px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -385px -144px}
#smart_editor2 .se2_qeditor .se2_mercol_off{width:57px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -413px -144px}
/* TEXT_TOOLBAR : QUICKEDITOR_TABLE > CELL_BACKGROUND */
#smart_editor2 .se2_qeditor .se2_qe2{_display:inline;float:left;position:relative;z-index:100;width:165px;margin:2px 0 0 1px;padding:7px 0 0 0;background:url("../img/bg_line1.gif") repeat-x;zoom:1}
#smart_editor2 .se2_qeditor .se2_qe2_1 dt{float:left;width:62px;padding:3px 0 0 0}
#smart_editor2 .se2_qeditor .se2_qe2_1 dt input{width:15px;height:15px;margin:-1px 1px 1px -1px;vertical-align:middle}
#smart_editor2 .se2_qeditor .se2_qe2_1 dd{float:left;position:relative;zoom:1}
#smart_editor2 .se2_qeditor .se2_qe2_3{padding:7px 0 6px 0}
/* My글양식 없을때 */
#smart_editor2 .se2_qeditor .se2_qe2_2{position:relative;_position:absolute}
#smart_editor2 .se2_qeditor .se2_qe2_2 dt{float:left;width:50px;padding:3px 0 0 13px}
#smart_editor2 .se2_qeditor .se2_qe2_2 dt input{width:15px;height:15px;margin:-1px 2px 1px -1px;vertical-align:middle}
#smart_editor2 .se2_qeditor .se2_qe2_2 dd{float:left}
/* TEXT_TOOLBAR : QUICKEDITOR_TABLE > STYLE */
#smart_editor2 .se2_table_set .se2_qbody .se2_t_proper2{float:left;*float:none;position:static;width:166px;margin:5px 0 0 1px}
#smart_editor2 .se2_qeditor .se2_qe3 dt{float:left;width:62px;padding:0}
#smart_editor2 .se2_qeditor .se2_qe3 dt label{font-weight:normal}
#smart_editor2 .se2_qeditor .se2_qe3 dt input{width:15px;height:15px;margin:-1px 1px 1px -1px;vertical-align:middle}
#smart_editor2 .se2_qeditor .se2_qe3 dd .se2_qe3_table{position:relative}
/* TEXT_TOOLBAR : QUICKEDITOR_TABLE > CELL_BACKGROUND PREWVIEW */
#smart_editor2 .se2_qeditor .se2_pre_color{float:left;width:18px;height:18px;border:1px solid #c7c7c7}
#smart_editor2 .se2_qeditor .se2_pre_color button{float:left;width:14px;height:14px;margin:2px 0 0 2px;padding:0}
#smart_editor2 .se2_qeditor .se2_pre_color button span{overflow:hidden;position:absolute;top:-10000px;left:-10000px;z-index:-100;width:0;height:0}
/* TEXT_TOOLBAR : QUICKEDITOR_TABLE > CELL_BACKGROUND LAYER */
#smart_editor2 .se2_qeditor .se2_layer{float:left;clear:both;position:absolute;top:20px;left:0;margin:0;padding:0;border:1px solid #c7c7c7;border-top:1px solid #9a9a9a;background:#fafafa}
#smart_editor2 .se2_qeditor .se2_layer .se2_in_layer{float:left;margin:0;padding:0;border:1px solid #fff;background:#fafafa}
#smart_editor2 .se2_qeditor .se2_layer button{vertical-align:top}
#smart_editor2 .se2_qeditor .se2_layer .se2_pick_color li{position:relative}
/* TEXT_TOOLBAR : QUICKEDITOR_TABLE > CELL_BACKGROUND IMAGE */
#smart_editor2 .se2_qeditor .se2_pre_bgimg{float:left;width:14px;height:14px;padding:2px;border:1px solid #c7c7c7}
#smart_editor2 .se2_qeditor .se2_qe2_2 button{width:16px;height:16px;background:url("../img/ko_KR/btn_set.png?130306") 0 -261px no-repeat}
/* TEXT_TOOLBAR : QUICKEDITOR_TABLE > CELL_BACKGROUND IMAGE LAYER */
#smart_editor2 .se2_cellimg_set{_display:inline;float:left;width:136px;margin:4px 3px 0 4px;padding-bottom:4px}
#smart_editor2 .se2_cellimg_set li{_display:inline;float:left;width:16px;height:16px;margin:0 1px 1px 0}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg0{background-position:-255px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg1{background-position:0 -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg2{background-position:-17px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg3{background-position:-34px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg4{background-position:-51px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg5{background-position:-68px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg6{background-position:-85px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg7{background-position:-102px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg8{background-position:-119px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg9{background-position:-136px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg10{background-position:-153px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg11{background-position:-170px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg12{background-position:-187px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg13{background-position:-204px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg14{background-position:-221px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg15{background-position:-238px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg16{background-position:-255px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg17{background-position:0 -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg18{background-position:-17px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg19{background-position:-34px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg20{background-position:-51px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg21{background-position:-68px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg22{background-position:-85px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg23{background-position:-102px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg24{background-position:-119px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg25{background-position:-136px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg26{background-position:-153px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg27{background-position:-170px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg28{background-position:-187px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg29{background-position:-204px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg30{background-position:-221px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg31{background-position:-238px -278px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg button{width:14px;height:14px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg1{background-position:-1px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg2{background-position:-18px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg3{background-position:-35px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg4{background-position:-52px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg5{background-position:-69px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg6{background-position:-86px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg7{background-position:-103px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg8{background-position:-120px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg9{background-position:-137px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg10{background-position:-154px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg11{background-position:-171px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg12{background-position:-188px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg13{background-position:-205px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg14{background-position:-222px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg15{background-position:-239px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg16{background-position:-256px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg17{background-position:-1px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg18{background-position:-18px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg19{background-position:-35px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg20{background-position:-52px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg21{background-position:-69px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg22{background-position:-86px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg23{background-position:-103px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg24{background-position:-120px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg25{background-position:-137px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg26{background-position:-154px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg27{background-position:-171px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg28{background-position:-188px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg29{background-position:-205px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg30{background-position:-222px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg31{background-position:-239px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg32{background-position:-256px -279px}
/* TEXT_TOOLBAR : QUICKEDITOR_TABLE > MY REVIEW */
#smart_editor2 .se2_btn_area{_display:inline;float:left;clear:both;width:166px;margin:5px 0 0 1px;padding:7px 0 6px 0;background:url("../img/bg_line1.gif") repeat-x;text-align:center}
#smart_editor2 .se2_btn_area .se2_btn_save{width:97px;height:21px;background:url("../img/ko_KR/btn_set.png?130306") -369px -163px no-repeat}
/* TEXT_TOOLBAR : QUICKEDITOR_IMAGE */
#smart_editor2 .se2_qe10{width:166px;margin:0;*margin:-2px 0 0 0}
#smart_editor2 .se2_qe10 label{margin:0 1px 0 0;vertical-align:middle}
#smart_editor2 .se2_qe10 .se2_sheight{margin-left:4px}
#smart_editor2 .se2_qe10 .input_ty1{width:30px;height:13px;margin:0 0 1px 1px;padding:3px 4px 0 1px;font-size:11px;letter-spacing:0;text-align:right;vertical-align:middle}
#smart_editor2 .se2_qe10 .se2_sreset{width:41px;height:19px;margin-left:3px;background:url("../img/ko_KR/btn_set.png?130306") -401px -184px no-repeat;vertical-align:middle}
#smart_editor2 .se2_qe10_1{margin-top:4px;padding:10px 0 3px;background:url("../img/bg_line1.gif") repeat-x}
#smart_editor2 .se2_qe10_1 input{width:15px;height:15px;margin:-1px 3px 1px -1px;vertical-align:middle}
#smart_editor2 .se2_qe11{float:left;width:166px;margin:4px 0 0 0;padding:7px 0 2px 0;background:url("../img/bg_line1.gif") repeat-x}
#smart_editor2 .se2_qe11_1{float:left;width:99px}
#smart_editor2 .se2_qe11_1 dt{float:left;width:56px;height:15px;padding:5px 0 0 0}
#smart_editor2 .se2_qe11_1 dd{float:left;position:relative;width:38px;height:20px}
#smart_editor2 .se2_qe11_1 .input_ty1{display:block;width:29px;height:15px;margin:0;*margin:-1px 0 1px 0;padding:3px 1px 0 5px;font-size:11px;letter-spacing:0;text-align:left}
#smart_editor2 .se2_qe11_1 .se2_add{position:absolute;top:2px;right:3px;width:13px;height:8px;background:url("../img/ko_KR/btn_set.png?130306") -86px -54px no-repeat}
#smart_editor2 .se2_qe11_1 .se2_del{position:absolute;top:10px;right:3px;width:13px;height:8px;background:url("../img/ko_KR/btn_set.png?130306") -86px -62px no-repeat}
#smart_editor2 .se2_qe11_2{float:left;width:67px}
#smart_editor2 .se2_qe11_2 dt{float:left;width:47px;margin:5px 0 0 0}
#smart_editor2 .se2_qe11_2 dd{float:left;position:relative;width:20px}
#smart_editor2 .se2_qe12{float:left;width:166px;margin:3px 0 0 0;padding:7px 0 0 0;background:url("../img/bg_line1.gif") repeat-x}
#smart_editor2 .se2_qe12 dt{float:left;margin:5px 4px 0 0}
#smart_editor2 .se2_qe12 dd{float:left;padding:0 0 6px 0}
#smart_editor2 .se2_qe12 .se2_align0{float:left;width:19px;height:21px;background:url("../img/ko_KR/btn_set.png?130306") -276px -121px no-repeat}
#smart_editor2 .se2_qe12 .se2_align1{float:left;width:19px;height:21px;background:url("../img/ko_KR/btn_set.png?130306") -295px -121px no-repeat}
#smart_editor2 .se2_qe12 .se2_align2{float:left;width:20px;height:21px;background:url("../img/ko_KR/btn_set.png?130306") -314px -121px no-repeat}
#smart_editor2 .se2_qe13{position:relative;z-index:10;zoom:1}
#smart_editor2 .se2_qe13 dt{float:left;width:62px;padding:3px 0 0}
#smart_editor2 .se2_qe13 dt input{width:15px;height:15px;margin:-1px 1px 1px -1px;vertical-align:middle;zoom:1}
#smart_editor2 .se2_qe13 dt .se2_qdim2{width:32px}
#smart_editor2 .se2_qe13 dd .se2_select_ty1{width:38px}
#smart_editor2 .se2_qe13 dd .se2_select_ty1 span{width:15px}
#smart_editor2 .se2_qe13 dd .input_ty1{width:20px}
#smart_editor2 .se2_qe13 dd .se2_palette2 .input_ty1{width:67px}
#smart_editor2 .se2_qe13 .se2_add{*top:3px}
#smart_editor2 .se2_qe13 .se2_del{*top:11px}
#smart_editor2 .se2_qe13 .se2_layer_b_style{right:-2px;_right:0}
#smart_editor2 .se2_qe13 .se2_layer_b_style li span{width:auto;margin:0 4px 0 5px;padding-top:2px}
#smart_editor2 .se2_qe13 dd{_display:inline;float:left;position:relative;width:29px;margin-right:5px;_margin-right:3px;zoom:1}
#smart_editor2 .se2_qe13 dd .se2_palette h4{margin-top:9px;font-family:dotum;font-size:12px}
#smart_editor2 .se2_qe13 dd.dd_type{width:38px}
#smart_editor2 .se2_qe13 dd.dd_type2{width:37px;margin-right:3px}
#smart_editor2 .se2_qe13 dd.dd_type2 .input_ty1{width:29px}
#smart_editor2 .se2_qe13 dd.dd_type2 button{right:2px;_right:1px}
#smart_editor2 .se2_qe13 dd.dd_type3{width:20px;margin:0}
#smart_editor2 .se2_qe13_v1{_display:inline;float:left;margin:2px 0 1px}
#smart_editor2 .se2_qe13_v1 dt{padding:4px 0 0 1px}
#smart_editor2 .se2_qe13_v2{_display:inline;float:left;position:relative;z-index:100;width:165px;margin:4px 0 0 1px;zoom:1}
#smart_editor2 .se2_qe13_v2 dd{width:18px;margin:0}
#smart_editor2 .se2_qeditor .se2_qdim1{clear:both;position:absolute;top:25px;left:115px;width:60px;height:23px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim2{clear:both;position:absolute;top:55px;left:24px;z-index:110;width:70px;height:22px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim3{clear:both;position:absolute;top:55px;left:118px;z-index:110;width:56px;height:22px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim4{clear:both;position:absolute;top:81px;left:23px;z-index:35;width:116px;height:35px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim5{clear:both;position:absolute;top:31px;left:106px;width:68px;height:26px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim6c{clear:both;position:absolute;top:25px;left:28px;width:29px;height:23px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim6r{clear:both;position:absolute;top:25px;left:57px;width:29px;height:23px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_highedit{float:right;width:56px;height:21px;margin:-27px 8px 0 0;background:url("../img/ko_KR/btn_set.png?130306") -329px -142px no-repeat}
#smart_editor2 .se2_qeditor .se2_qdim7{clear:both;position:absolute;top:55px;left:24px;z-index:110;width:150px;height:48px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim8{clear:both;position:absolute;top:105px;left:24px;z-index:110;width:150px;height:37px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim9{clear:both;position:absolute;top:55px;left:111px;z-index:110;width:65px;height:24px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim10{clear:both;position:absolute;top:55px;left:100px;z-index:110;width:77px;height:24px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim11{clear:both;position:absolute;top:55px;left:65px;z-index:110;width:115px;height:24px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
/* HELP : ACCESSIBILITY */
#smart_editor2 .se2_accessibility{z-index:90}
#smart_editor2 .se2_accessibility .se2_in_layer{width:568px;padding:0 10px;background:#fafafa;border:1px solid #bcbbbb}
#smart_editor2 .se2_accessibility h3{margin:0 -10px;padding:6px 0 12px 0;background:url("../img/bg_find_h3.gif") repeat-x;font-size:12px;line-height:14px;letter-spacing:-1px}
#smart_editor2 .se2_accessibility h3 strong{display:inline-block;padding:4px 0 3px 11px;color:#333;letter-spacing:0}
#smart_editor2 .se2_accessibility .se2_close{position:absolute;top:10px;right:12px;width:13px;height:12px;background:url("../img/ko_KR/btn_set.png?130306") -155px -5px no-repeat}
#smart_editor2 .se2_accessibility .box_help{padding:0 2px;margin-top:8px;background:url("../img/bg_help.gif") 0 100% no-repeat}
#smart_editor2 .se2_accessibility .box_help div{overflow:hidden;padding:20px 21px 24px;border-top:1px solid #d0d0d0;color:#333}
#smart_editor2 .se2_accessibility .box_help strong{display:block;margin-bottom:2px}
#smart_editor2 .se2_accessibility .box_help p{margin-bottom:28px;line-height:1.5}
#smart_editor2 .se2_accessibility .box_help ul{width:150%;margin-top:10px}
#smart_editor2 .se2_accessibility .box_help li{position:relative;float:left;width:252px;padding:5px 0 5px 9px;margin-right:40px;background:url("../img/ko_KR/btn_set.png?130306") -475px -51px no-repeat;border-right:1px solid #f0f0f0;*zoom:1;line-height:1}
#smart_editor2 .se2_accessibility .box_help li span{position:absolute;top:4px;left:138px;line-height:1.2}
#smart_editor2 .se2_accessibility .se2_btns{padding:9px 0 10px;text-align:center}
#smart_editor2 .se2_accessibility .se2_btns .se2_close2{width:39px;height:24px;background:url("../img/ko_KR/btn_set.png?130306") -235px -120px no-repeat}

View File

@ -1,12 +0,0 @@
@charset "UTF-8";
/* NHN Web Standardization Team (http://html.nhndesign.com/) HHJ 090226 */
/* COMMON */
.se2_outputarea,.se2_outputarea th,.se2_outputarea td{margin:0;padding:0;color:#666;font-size:12px;font-family:'돋움',Dotum,'굴림',Gulim,Helvetica,Sans-serif;line-height:1.5}
.se2_outputarea p{margin:0;padding:0}
.se2_outputarea a:hover{text-decoration:underline}
.se2_outputarea a:link{color:#0000ff}
.se2_outputarea ul{margin:0 0 0 40px;padding:0}
.se2_outputarea ul li{margin:0;list-style-type:disc;padding:0}
.se2_outputarea ul ul li{list-style-type:circle}
.se2_outputarea ul ul ul li{list-style-type:square}
.se2_outputarea img,.se2_outputarea fieldset{border:0}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 526 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -1,134 +0,0 @@
if(typeof window.nhn=='undefined') window.nhn = {};
if (!nhn.husky) nhn.husky = {};
/**
* @fileOverview This file contains application creation helper function, which would load up an HTML(Skin) file and then execute a specified create function.
* @name HuskyEZCreator.js
*/
nhn.husky.EZCreator = new (function(){
this.nBlockerCount = 0;
this.createInIFrame = function(htOptions){
if(arguments.length == 1){
var oAppRef = htOptions.oAppRef;
var elPlaceHolder = htOptions.elPlaceHolder;
var sSkinURI = htOptions.sSkinURI;
var fCreator = htOptions.fCreator;
var fOnAppLoad = htOptions.fOnAppLoad;
var bUseBlocker = htOptions.bUseBlocker;
var htParams = htOptions.htParams || null;
}else{
// for backward compatibility only
var oAppRef = arguments[0];
var elPlaceHolder = arguments[1];
var sSkinURI = arguments[2];
var fCreator = arguments[3];
var fOnAppLoad = arguments[4];
var bUseBlocker = arguments[5];
var htParams = arguments[6];
}
if(bUseBlocker) nhn.husky.EZCreator.showBlocker();
var attachEvent = function(elNode, sEvent, fHandler){
if(elNode.addEventListener){
elNode.addEventListener(sEvent, fHandler, false);
}else{
elNode.attachEvent("on"+sEvent, fHandler);
}
}
if(!elPlaceHolder){
alert("Placeholder is required!");
return;
}
if(typeof(elPlaceHolder) != "object")
elPlaceHolder = document.getElementById(elPlaceHolder);
var elIFrame, nEditorWidth, nEditorHeight;
try{
elIFrame = document.createElement("<IFRAME frameborder=0 scrolling=no>");
}catch(e){
elIFrame = document.createElement("IFRAME");
elIFrame.setAttribute("frameborder", "0");
elIFrame.setAttribute("scrolling", "no");
}
elIFrame.style.width = "1px";
elIFrame.style.height = "1px";
elPlaceHolder.parentNode.insertBefore(elIFrame, elPlaceHolder.nextSibling);
attachEvent(elIFrame, "load", function(){
fCreator = elIFrame.contentWindow[fCreator] || elIFrame.contentWindow.createSEditor2;
// top.document.title = ((new Date())-window.STime);
// window.STime = new Date();
try{
nEditorWidth = elIFrame.contentWindow.document.body.scrollWidth || "500px";
nEditorHeight = elIFrame.contentWindow.document.body.scrollHeight + 12;
elIFrame.style.width = "100%";
elIFrame.style.height = nEditorHeight+ "px";
elIFrame.contentWindow.document.body.style.margin = "0";
}catch(e){
nhn.husky.EZCreator.hideBlocker(true);
elIFrame.style.border = "5px solid red";
elIFrame.style.width = "500px";
elIFrame.style.height = "500px";
alert("Failed to access "+sSkinURI);
return;
}
var oApp = fCreator(elPlaceHolder, htParams); // oEditor
oApp.elPlaceHolder = elPlaceHolder;
oAppRef[oAppRef.length] = oApp;
if(!oAppRef.getById) oAppRef.getById = {};
if(elPlaceHolder.id) oAppRef.getById[elPlaceHolder.id] = oApp;
oApp.run({fnOnAppReady:fOnAppLoad});
// top.document.title += ", "+((new Date())-window.STime);
nhn.husky.EZCreator.hideBlocker();
});
// window.STime = new Date();
elIFrame.src = sSkinURI;
this.elIFrame = elIFrame;
};
this.showBlocker = function(){
if(this.nBlockerCount<1){
var elBlocker = document.createElement("DIV");
elBlocker.style.position = "absolute";
elBlocker.style.top = 0;
elBlocker.style.left = 0;
elBlocker.style.backgroundColor = "#FFFFFF";
elBlocker.style.width = "100%";
document.body.appendChild(elBlocker);
nhn.husky.EZCreator.elBlocker = elBlocker;
}
nhn.husky.EZCreator.elBlocker.style.height = Math.max(document.body.scrollHeight, document.body.clientHeight)+"px";
this.nBlockerCount++;
};
this.hideBlocker = function(bForce){
if(!bForce){
if(--this.nBlockerCount > 0) return;
}
this.nBlockerCount = 0;
if(nhn.husky.EZCreator.elBlocker) nhn.husky.EZCreator.elBlocker.style.display = "none";
}
})();

View File

@ -1,93 +0,0 @@
function createSEditor2(elIRField, htParams, elSeAppContainer){
if(!window.$Jindo){
parent.document.body.innerHTML="진도 프레임웍이 필요합니다.<br>\n<a href='http://dev.naver.com/projects/jindo/download'>http://dev.naver.com/projects/jindo/download</a>에서 Jindo 1.5.3 버전의 jindo.min.js를 다운로드 받아 /js 폴더에 복사 해 주세요.\n(아직 Jindo 2 는 지원하지 않습니다.)";
return;
}
var elAppContainer = (elSeAppContainer || jindo.$("smart_editor2"));
var elEditingArea = jindo.$$.getSingle("DIV.husky_seditor_editing_area_container", elAppContainer);
var oWYSIWYGIFrame = jindo.$$.getSingle("IFRAME.se2_input_wysiwyg", elEditingArea);
var oIRTextarea = elIRField?elIRField:jindo.$$.getSingle("TEXTAREA.blind", elEditingArea);
var oHTMLSrc = jindo.$$.getSingle("TEXTAREA.se2_input_htmlsrc", elEditingArea);
var oTextArea = jindo.$$.getSingle("TEXTAREA.se2_input_text", elEditingArea);
if(!htParams){
htParams = {};
htParams.fOnBeforeUnload = null;
}
htParams.elAppContainer = elAppContainer; // 에디터 UI 최상위 element 셋팅
htParams.oNavigator = jindo.$Agent().navigator(); // navigator 객체 셋팅
var oEditor = new nhn.husky.HuskyCore(htParams);
oEditor.registerPlugin(new nhn.husky.CorePlugin(htParams?htParams.fOnAppLoad:null));
oEditor.registerPlugin(new nhn.husky.StringConverterManager());
var htDimension = {
nMinHeight:205,
nMinWidth:parseInt(elIRField.style.minWidth, 10)||570,
nHeight:elIRField.style.height||elIRField.offsetHeight,
nWidth:elIRField.style.width||elIRField.offsetWidth
};
var htConversionMode = {
bUseVerticalResizer : htParams.bUseVerticalResizer,
bUseModeChanger : htParams.bUseModeChanger
};
var aAdditionalFontList = htParams.aAdditionalFontList;
oEditor.registerPlugin(new nhn.husky.SE_EditingAreaManager("WYSIWYG", oIRTextarea, htDimension, htParams.fOnBeforeUnload, elAppContainer));
oEditor.registerPlugin(new nhn.husky.SE_EditingArea_WYSIWYG(oWYSIWYGIFrame)); // Tab Editor 모드
oEditor.registerPlugin(new nhn.husky.SE_EditingArea_HTMLSrc(oHTMLSrc)); // Tab HTML 모드
oEditor.registerPlugin(new nhn.husky.SE_EditingArea_TEXT(oTextArea)); // Tab Text 모드
oEditor.registerPlugin(new nhn.husky.SE2M_EditingModeChanger(elAppContainer, htConversionMode)); // 모드간 변경(Editor, HTML, Text)
oEditor.registerPlugin(new nhn.husky.SE_PasteHandler()); // WYSIWYG Paste Handler
oEditor.registerPlugin(new nhn.husky.HuskyRangeManager(oWYSIWYGIFrame));
oEditor.registerPlugin(new nhn.husky.Utils());
oEditor.registerPlugin(new nhn.husky.SE2M_UtilPlugin());
oEditor.registerPlugin(new nhn.husky.SE_WYSIWYGStyler());
oEditor.registerPlugin(new nhn.husky.SE2M_Toolbar(elAppContainer));
oEditor.registerPlugin(new nhn.husky.Hotkey()); // 단축키
oEditor.registerPlugin(new nhn.husky.SE_EditingAreaVerticalResizer(elAppContainer, htConversionMode)); // 편집영역 리사이즈
oEditor.registerPlugin(new nhn.husky.DialogLayerManager());
oEditor.registerPlugin(new nhn.husky.ActiveLayerManager());
oEditor.registerPlugin(new nhn.husky.SE_WYSIWYGStyleGetter()); // 커서 위치 스타일 정보 가져오기
oEditor.registerPlugin(new nhn.husky.SE_WYSIWYGEnterKey("P")); // 엔터 시 처리, 현재는 P로 처리
oEditor.registerPlugin(new nhn.husky.SE2M_ColorPalette(elAppContainer)); // 색상 팔레트
oEditor.registerPlugin(new nhn.husky.SE2M_FontColor(elAppContainer)); // 글자색
oEditor.registerPlugin(new nhn.husky.SE2M_BGColor(elAppContainer)); // 글자배경색
oEditor.registerPlugin(new nhn.husky.SE2M_FontNameWithLayerUI(elAppContainer, aAdditionalFontList)); // 글꼴종류
oEditor.registerPlugin(new nhn.husky.SE2M_FontSizeWithLayerUI(elAppContainer)); // 글꼴크기
oEditor.registerPlugin(new nhn.husky.SE2M_LineStyler());
oEditor.registerPlugin(new nhn.husky.SE2M_ExecCommand(oWYSIWYGIFrame));
oEditor.registerPlugin(new nhn.husky.SE2M_LineHeightWithLayerUI(elAppContainer)); // 줄간격
oEditor.registerPlugin(new nhn.husky.SE2M_Quote(elAppContainer)); // 인용구
oEditor.registerPlugin(new nhn.husky.SE2M_Hyperlink(elAppContainer)); // 링크
oEditor.registerPlugin(new nhn.husky.SE2M_SCharacter(elAppContainer)); // 특수문자
oEditor.registerPlugin(new nhn.husky.SE2M_FindReplacePlugin(elAppContainer)); // 찾기/바꾸기
oEditor.registerPlugin(new nhn.husky.SE2M_TableCreator(elAppContainer)); // 테이블 생성
oEditor.registerPlugin(new nhn.husky.SE2M_TableEditor(elAppContainer)); // 테이블 편집
oEditor.registerPlugin(new nhn.husky.SE2M_TableBlockStyler(elAppContainer)); // 테이블 스타일
if(nhn.husky.SE2M_AttachQuickPhoto){
oEditor.registerPlugin(new nhn.husky.SE2M_AttachQuickPhoto(elAppContainer)); // 사진
}
oEditor.registerPlugin(new nhn.husky.MessageManager(oMessageMap));
oEditor.registerPlugin(new nhn.husky.SE2M_QuickEditor_Common(elAppContainer)); // 퀵에디터 공통(표, 이미지)
oEditor.registerPlugin(new nhn.husky.SE2B_CSSLoader()); // CSS lazy load
if(window.frameElement){
oEditor.registerPlugin(new nhn.husky.SE_OuterIFrameControl(elAppContainer, 100));
}
oEditor.registerPlugin(new nhn.husky.SE_ToolbarToggler(elAppContainer, htParams.bUseToolbar));
oEditor.registerPlugin(new nhn.husky.SE2M_Accessibility(elAppContainer)); // 에디터내의 웹접근성 관련 기능모음 플러그인
return oEditor;
}

View File

@ -1,52 +0,0 @@
/*
* Smart Editor 2 Configuration : This setting must be changed by service
*/
window.nhn = window.nhn || {};
nhn.husky = nhn.husky || {};
nhn.husky.SE2M_Configuration = nhn.husky.SE2M_Configuration || {};
/**
* CSS LazyLoad를 위한 경로
*/
nhn.husky.SE2M_Configuration.SE2B_CSSLoader = {
sCSSBaseURI : "css"
};
/**
* 편집영역 설정
*/
nhn.husky.SE2M_Configuration.SE_EditingAreaManager = {
sCSSBaseURI : "css", // smart_editor2_inputarea.html 파일의 상대경로
sBlankPageURL : "smart_editor2_inputarea.html",
sBlankPageURL_EmulateIE7 : "smart_editor2_inputarea_ie8.html",
aAddtionalEmulateIE7 : [] // IE8 default 사용, IE9 ~ 선택적 사용
};
/**
* [웹접근성]
* 단축키 ALT+, ALT+. 이용하여 스마트에디터 영역의 이전/이후 요소로 이동할 있다.
* sBeforeElementId : 스마트에디터 영역 이전 요소의 id
* sNextElementId : 스마트에디터 영역 이후 요소의 id
*
* 스마트에디터 영역 이외의 제목 영역 (:스마트에디터가 적용된 블로그 쓰기 페이지에서의 제목 영역) 해당하는 엘리먼트에서 Tab키를 누르면 에디팅 영역으로 포커스를 이동시킬 있다.
* sTitleElementId : 제목에 해당하는 input 요소의 id.
*/
nhn.husky.SE2M_Configuration.SE2M_Accessibility = {
sBeforeElementId : '',
sNextElementId : '',
sTitleElementId : ''
};
/**
* 링크 기능 옵션
*/
nhn.husky.SE2M_Configuration.SE2M_Hyperlink = {
bAutolink : true // 자동링크기능 사용여부(기본값:true)
};
nhn.husky.SE2M_Configuration.Quote = {
sImageBaseURL : 'http://static.se2.naver.com/static/img'
};
nhn.husky.SE2M_Configuration.SE2M_ColorPalette = {
bAddRecentColorFromDefault : false
};

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 it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,40 +0,0 @@
SmartEditor Basic 2.0 릴리즈 패키지
SmartEdtitor™는 Javascript로 구현된 웹 기반의 WYSIWYG 에디터입니다. SmartEdtitor™는 WYSIWYG 모드 및 HTML 편집 모드와 TEXT 모드를 제공하고, 자유로운 폰트 크기 설정 기능, 줄 간격 설정 기능, 단어 찾기/바꾸기 기능 등 편집에 필요한 다양한 기능을 제공하므로 사용자들은 SmartEdtitor™를 사용하여 쉽고 편리하게 원하는 형태의 글을 작성할 수 있습니다.
또한, SmartEdtitor™의 구조는 기능을 쉽게 추가할 수 있는 플러그인 구조로 되어 있어 정해진 규칙에 따라 플러그인을 만들기만 하면 됩니다.
현재 SmartEdtitor™는 네이버, 한게임 등 NHN의 주요 서비스에 적용되어 있습니다.
지원하는 브라우저 환경은 아래와 같으며 지속적으로 지원 대상 브라우저를 확장할 예정입니다.
* 지원하는 브라우저
Internet Explorer 7.0+ / 10.0-
FireFox 3.5+
Safari 4.0+
Chrome 4.0+
또한 지속적인 기능 추가를 통해 편리하고 강력한 에디터로 거듭날 것입니다.
라이센스 : LGPL v2
홈페이지 : http://dev.naver.com/projects/smarteditor
===================================================================================
릴리즈 패키지에 포함된 파일은 아래와 같습니다.
/css : 에디터에서 사용하는 css 파일
/img : 에디터에서 사용하는 이미지 파일
/js : 에디터를 적용할 때 사용하는 JS 파일
/photo_uploader : 사진 퀵 업로더 팝업 UI를 구성하는 파일
readme.txt : 간략한 설명
release_notes.txt : 릴리즈 노트
sample.php : SmartEditor2.html을 이용해 편집한 내용을 서버에서 받는 php 예제
smart_editor2_inputarea.html : 에디터의 편집 영역을 나타내는 HTML로 에디터를 적용할 때 반드시 필요
smart_editor2_inputarea_ie8.html : smart_editor2_inputarea.html와 동일한 기능이나 사용자의 브라우저 Internet Explorer 8.x 이상인 경우에 사용
SmartEditor2.html : 에디터 데모 페이지. 에디터 적용 시에도 참고 할 수 있다.
SmartEditor2Skin.html : 에디터를 적용한 페이지에서 로드하는 에디터의 스킨 HTML 파일로 에디터에서 사용하는 JS 파일과 css 파일을 링크하며 에디터의 마크업을 가지고 있다. SmartEditor2.html 에서도 확인할 수 있다.
src_include.txt : 자바스크립트 플러그인 소스를 직접 수정하고자 할 경우 참고할 수 있는 파일
===================================================================================
사용 중 불편한 점이 있거나 버그를 발견하는 경우 SmartEdtitor™ 프로젝트의 이슈에 올려 주세요~~~
http://dev.naver.com/projects/smarteditor/issue
여기입니다! :)

View File

@ -1,255 +0,0 @@
==============================================================================================
2.3.10_임시
----------------------------------------------------------------------------------------------
1. 버그 수정
- 크롬 > 밑줄 선택 글작성하다 취소선 선택하고 밑줄 선택을 취소한 경우 툴바에 반영되지 않는 문제
- 굵게/밑줄/기울림/취소선이 있는 상태에서 엔터치고 폰트크기 수정하면 이전 폰트크기로 줄간격이 유지되는 문제
- 외부프로그램 테이블 복사 붙여넣기 관련 오류 수정
- IE8이하 > 글자크기 지정 후 엔터를 치면 커서위치가 위로 올라감
- IE9이상 > 글꼴 효과를 미리 지정 한 후에 텍스트 입력 시, 색상 변경은 적용되나 굵게 기울임 밑줄 취소선 등의 효과는 적용안됨
- [FF]밑줄 선택> 내용입력 후 엔터>밑줄 취소 후 내용 입력>마우스로 커서 클릭 후 내용 계속 입력 시 밑줄이 있는 글로 노출됨
- [FF] 메모장에서 작성한 내용을 붙여넣기 후 엔터 > 내용입력 > 엔터 했을 때 줄바꿈이 되지 않는 현상
- HTML5 > 글자를 선택하여 폰트크기 지정시 굵게/밑줄/기울림/취소선이 있으면 이전에 적용한 폰트크기 기준으로 줄간격이 유지되는 문제
2. 기능 개선
- IE에서 자동으로 공백이 삽입되는 문제
- MacOS > 사파리 > 외부프로그램 테이블 붙여넣기 개선
3. 보안 패치
- 사진첨부 샘플의 null byte injection 취약점 보완
==============================================================================================
2.3.10
----------------------------------------------------------------------------------------------
1. 버그 수정
- 크롬 > 브라우저 확대축소시 폰트크기가 잘못 나오는 이슈
- IE > 표삽입>임의로 두개 칸 선택하여 셀 병합>행삽입 클릭 시 JS 오류 발생
- IE11 > 호환성 보기를 설정하지 않을 경우 글꼴목록이 선택되지 않는 문제 수정
2. 기능 개선
- 외부프로그램 테이블 복사 붙여넣기 개선
- 입력창 조절 안내 레이어를 주석처리하면 스크립트 오류 발생
==============================================================================================
2.3.9
----------------------------------------------------------------------------------------------
1. 버그 수정
- 파이어폭스에서 에디팅시 스타일깨짐 등 오작동
- Chrome > 찾기/바꾸기 > 모두바꾸기 버튼 클릭시 찾을단어가 지워지지 않고 남아있음
2. 기능 개선
- 링크 > 자동링크 설정/해제 옵션 추가
- [IE11] WYSIWYG 모드와 HTML 모드를 오갈 때마다 문서의 마지막에 비정상적인 <BR>이 첨가됩니다.
- [웹접근성] 빠져나가기 단축키 기능 개선
==============================================================================================
2.3.8
----------------------------------------------------------------------------------------------
1. 버그 수정
- 테이블 내부 영역을 Shift + 클릭으로 선택 후 정렬하고 HTML 로 전환하면 더미 P 태그가 생성되는 문제 수정
- 테이블 내부 영역 선택 혹은 에디터 내용 전체 선택 후 정렬 시 동작안함
- [IE10, IE11] 표의 셀을 드래그했을 때 블럭 지정이 되지 않는 현상
- HTML 모드 변환시 태그 자동 정렬에 의한 버그
2. 기능 개선
- [MacOS 대응] 폰트변경이슈
==============================================================================================
2.3.7
----------------------------------------------------------------------------------------------
1. 버그 수정
- 에디터에 표 생성 후 일부 셀 선택하여 배경색 설정> 배경색 설정된 셀 선택 후 셀 삽입 시 색상이 삽입되지 않습니다.
- [IE9특정] 글 작성 중 번호매기기 또는 글머리 적용 후 정렬방식을 변경하면 엔터키 누를 시 커서가 한줄 떨어져서 노출됩니다.
- [IE10] 표 생성 후 표 드래그 시 셀의 너비/높이가 늘어나는 현상
2. 기능 개선
- IE11 대응
- 특수기호 삽입시 커서 위치가 뒤쪽으로 나오도록 개선
- 커서에 활성화된 글꼴 확인 로직 개선
==============================================================================================
2.3.6
----------------------------------------------------------------------------------------------
1. 버그 수정
- 글 작성 후 번호매기기 적용하고 엔터키 수행하는 경우 JS 오류가 발생하는 현상 수정
==============================================================================================
2.3.5
----------------------------------------------------------------------------------------------
1. 기능 개선
- 줄간격 설정 시 값을 직접 입력하는 경우 줄간격의 최소값 적용
==============================================================================================
2.3.4
----------------------------------------------------------------------------------------------
1. 버그 수정
- [IE9/10] pre 태그의 바로 다음에 \n이 존재하는 경우 개행이 되지 않는 이슈 해결
- 입력창 크기 조절바 사용 여부 오류 해결
- 사진 퀵 업로더 모듈 오타 수정 ($newPath -> $new_path)
2. 기능 개선
- 글꼴 목록에 글꼴 종류 추가하기 기능 (SmartEditor2.html 참조)
- 사진 퀵 업로더 모듈에 이미지 파일 확장자 체크 추가
==============================================================================================
2.3.3
----------------------------------------------------------------------------------------------
1. 버그 수정
- IE9 에서 템플릿을 적용한 표 생성 후 일부의 셀을 드래그하는 경우 셀의 높이가 늘어나는 현상 수정
2. 기능 개선
- MAC OS의 CMD 키로 Ctrl 단축키 기능 적용 확장
- 기본 글꼴 종류 추가 (Courier New, 나눔고딕 코딩)
==============================================================================================
2.3.1
----------------------------------------------------------------------------------------------
1. 기능 개선
- [웹접근성] 글쓰기 영역의 iframe의 title속성에 단축키 설명 제공
- [웹접근성] 제목 input영역에서 제목 입력 후 TAB하면 스마트에디터 편집 영역으로 포커스 이동하는 기능 추가
- [웹접근성] 툴바 영역의 이전/다음 아이템 이동을 TAB, SHIFT+TAB으로 이동할 수 있도록 추가
==============================================================================================
2.3.0
----------------------------------------------------------------------------------------------
1. 기능 개선
- [웹접근성] 키보드로만 메뉴를 이동할 수 있도록 단축키 적용
- [웹접근성] 웹접근성 도움말 제공
- 편집모드와 사이즈 조절바 사용 옵션 추가
- 사진 첨부 팝업 데모 파일 구조 개선
==============================================================================================
2.2.1
----------------------------------------------------------------------------------------------
1. 버그 수정
- 사진 퀵 업로더 추가 시, 가이드 대로 수행했을 때 사진 첨부가 2번 실행되는 문제 해결
: loader-min.js 파일 내에 사진 퀵 업로더 소스가 포함되어 있던 부분 제거하여 소스 분리
2. 기능 개선
- 툴바의 기능 제거/순서 변경이 쉽도록 마크업 구조 개선
※ 툴바의 기능 제거/순서 변경은 가이드 문서를 참고하세요.
3. 폴더/파일 변경
- /js_src 폴더 제거
- /js/smarteditor2.js 추가
: /js_src 폴더를 /js/smarteditor2.js 로 대체했습니다.
: /js_src 폴더 구조에서 사용자가 소스를 검색하여 수정하기 어렵던 부분을 보완하기 위하여
: /js_src 폴더 내의 플러그인 소스를 통합한 /js/smarteditor2.js 를 추가했습니다.
- /js/loader-min.js 제거
- /js/smarteditor2.min.js 추가
: /js/loader-min.js 파일을 /js/smarteditor2.min.js로 대체했습니다.
- /quick_photo_uploader 폴더 추가
- /popup 폴더 이동
: /popup 폴더 - 사진 퀵 업로더의 팝업과 관련된 소스
: /plugin 폴더 - 사진 퀵 업로더의 사진첨부를 처리하는 플러그인 js 소스
- /img/ko_KR 폴더 추가
: 이후의 다국어 버전 지원을 위하여 이미지 폴더 내 디렉토리가 추가되었습니다.
: 언어 별 구분이 필요없는 이미지는 /img 바로 하위에 두었고,
: 언어 별로 구분되어야 하는 이미지는 /img/ko_KR 과 같이 언어 별 디렉토리로 구분했습니다.
: 버전 업그레이드를 하는 경우 이미지 경로가 변경된 점에 주의하시기 바랍니다.
- /js/SE2B_Configuration.js 제거
- /js/SE2B_Configuration_Service.js 추가
- /js/SE2B_Configuration_General.js 추가
: /js/SE2B_Configuration_Service.js 와 /js/SE2B_Configuration_General.js로 파일 분리했습니다.
: /js/SE2B_Configuration_Service.js 는 적용을 할 때 사용자가 변경할 가능성이 높은 플러그인 설정을 갖고,
: /js/SE2B_Configuration_General.js 는 서비스에 적용할 때 변경할 가능성이 거의 없는 설정입니다.
==============================================================================================
2.1.3
----------------------------------------------------------------------------------------------
1. 버그 수정
- [Chrome] 보기 페이지에 글자색이 설정되어 있는 경우 글 작성 시 내용에 적용한 글자색으로 노출되지 않는 문제 해결
- 엔터 처리가 <BR>로 설정된 경우에도 텍스트 모드에서 모드변경 혹은 글 저장할 때 개행이 <P>로 표시되는 문제 해결
- [IE9] 각주 삽입 시, 하단으로 떨어지는 이슈 해결
- [Chrome] 인용구 밖에 글머리기호/번호매기기가 있을 때 인용구 안에서 글머리기호/번호매기기 시 내용이 인용구 밖으로 나가는 문제 해결
- [IE] IE에서 특정 블로그 글을 복사하여 붙여넣기 했을 때 개행이 제거되는 문제 해결
- 사진을 드래그해서 사이즈를 변경한 후 저장 혹은 HTML모드로 변경하면, 사진 사이즈가 원복되는 현상 해결
- [Chrome/FF/Safari] 스크롤바가 생성되도록 문자입력 후 엔터 클릭하지 않은 상태에서 이미지 하나 삽입 시 이미지에 포커싱이 놓이지 않는 문제 해결
- [IE9 표준] 사진을 스크롤로 일부 가린 상태에서 재편집하여 적용했을 때 계속 가려진 상태인 문제 해결
- FF에서 사진을 여러장 첨부 시 스크롤이 가장 마지막 추가한 사진으로 내려가지 않음 해결
- 호환 모드를 제거하고 사진 첨부 시 에디팅 영역의 커서 주위에 <sub><sup> 태그가 붙어서 글자가 매우 작게 되는 현상 해결
- [IE9] 에디터에 각주 연속으로 입력 시 커서가 각주사이로 이동되는 현상 해결
- 글꼴색/글꼴배경색 더보기에서 글꼴색 선택>다시 다른 색상 선택 후 처음 선택되었던 색상 선택 시 처음 선택색상이 원래 자리에서 삭제되지 않는 현상 해결
- 제공하지 않는 기능인 이모티콘 플러그인 소스 제거
- 플러그인 태그 코드 추가 시 <li> 태그와 <button> 태그 사이에 개행이 있으면 이벤트가 등록되지 않는 현상 해결
2. 기능 개선
- 표 삽입 시 본문 작성 영역 안에 너비 100%로 생성되도록 개선
- 호환모드 설정이 설정 파일 정보에 따라 처리되도록 변경
==============================================================================================
2.1.2
----------------------------------------------------------------------------------------------
1. 버그 수정
- [IE9]Shift+Enter를 여러번 하고 글의 중간의 마지막 글자 다음에서 엔터를 쳤을 때 엔터 위치가 달라지는 현상 수정
- [IE9]메모장에서 붙여 넣기 후 내용 중간의 마지막 글자 다음에서 엔터를 쳤을 때 엔터 위치가 달라지는 현상 수정
- 한 줄 입력 후 색상을 적용하고 내용 중간에서 엔터를 쳤을 때 적용되었던 색상이 풀리던 현상 수정
- 글꼴 레이어를 열었을 때, 샘플 텍스트가 잘못 나오던 현상 수정
- 인용구를 14개까지 중첩하고, 15개부터 경고 창이 나오도록 수정
2. 기능 개선
- 찾기/바꾸기 레이어를 닫았다가 다시 열 때, [바꿀 단어] 입력란이 초기화 되도록 개선
- 찾기/바꾸기 레이어 오픈 시 툴바 버튼 inactive 처리
- 표 추가 레이어의 테이블 색상, 배경 색상의 기본 값을 SmartEditor2Skin.html에서 변경할 수 있도록 함
※주의 : 기존의 html파일에 덮어 씌우게 되면 기본 배경 색상이 다르게 표시됨
따라서 반드시 새로 업데이트 된 html 파일을 사용하기를 권장
임의로 수정하려면 위 파일의 아래 부분의 value를 아래와 같이 변경해야 함
<input id="se2_b_color" name="" type="text" maxlength="7" value="#cccccc" class="input_ty3">
<input id="se2_cellbg" name="" type="text" maxlength="7" value="#ffffff" class="input_ty3">
==============================================================================================
2.1.1
----------------------------------------------------------------------------------------------
1. 기능 추가
- 에디터 로딩 완료 시점에 실행되는 함수 (fOnAppLoad) 정의
2. 버그 수정
- 에디터 초기 Width에 100%가 설정될 수 있도록 수정, minWidth 설정 추가
- 마크업에서 나눔 글꼴을 제외하면 JS 에러가 나는 문제 수정
- [IE9] 글자 색상 적용 후 내용 중간에서 계속 Enter할 때 Enter가 되지 않는 오류 수정
- [Chrome/Safari] 표 간단편집기 위에서 text를 drag하면 JS 에러가 발생하는 문제 수정
3. 기능 개선
- 사진 퀵 업로더 : 쉽게 사용할 수 있도록 소스 수정 및 예제 보강
==============================================================================================
2.1.0
----------------------------------------------------------------------------------------------
1. 기능 추가
- 사진 퀵 업로더 : 사진 첨부 팝업 UI 제공 (HTML5 지원)
- 에디터 본문에 글 작성 후 창을 닫을 때 발생하는 alert 메세지를 사용자가 설정할 수 있도록 옵션을 추가함
- Jindo 모듈을 패키지에 포함하도록 빌드를 수정함
- document.domain을 제거함
- 에디터 초기 Width를 설정할 수 있도록 수정함
- 툴바의 접힘/펼침 기능을 제공하는 SE_ToolbarToggler 플러그인 추가함
2. 버그 수정
- 에디터 리사이즈 시 북마크 태그가 본문에 추가되는 이슈 확인 및 수정함
==============================================================================================
2.0.0
----------------------------------------------------------------------------------------------
1. 기능 강화
- 글꼴과 글자 크기
: 기존의 Selectbox 형태의 글꼴 목록을 깔끔한 디자인의 레이어로 제공한다.
- 글자색과 글자 배경색
: 기존의 기본 색상표 이외에 다양한 색상을 선택할 수 있는 컬러 팔레트를 확장 지원한다.
- 줄간격
: 기존의 Selectbox 형태의 줄간격 목록을 깔끔한 디자인의 레이어로 제공한다.
또한, 줄간격을 직접 설정할 수 있도록 직접 입력 기능도 확장 지원한다.
- 인용구
: 기존의 7가지에서 10가지로 인용구 디자인을 확장 지원한다.
- 표
: 표 생성 시 기존의 테두리 색상과 두께를 설정할 수 있는 기능 이외에 테두리 스타일을 설정할 수 있는 기능을 확장 지원한다.
또한, 표 템플릿을 제공하여 보다 쉽게 표 스타일을 생성할 수 있도록 하였다.
2. 기능 추가
- 표 간단편집기
: 표 생성 후 스타일을 편집할 수 있도록 표 편집 기능을 추가 제공한다.
- TEXT 모드
: WYSIWYG와 HTML 모드 이외에 TEXT 모드를 제공하여 텍스트만으로 본문의 내용을 작성할 수 있도록 편집 모드를 추가 제공한다.

View File

@ -1,33 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="ko" xml:lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Smart Editor&#8482; WYSIWYG Mode</title>
<link href="css/smart_editor2_in.css" rel="stylesheet" type="text/css">
</head>
<body class="smartOutput se2_inputarea">
<p>
<b><u>에디터 내용:</u></b>
</p>
<div style="width:736px;">
<?php
$postMessage = $_POST["ir1"];
echo $postMessage;
?>
</div>
<hr>
<p>
<b><span style="color:#FF0000">주의: </span>sample.php는 샘플 파일로 정상 동작하지 않을 있습니다. 주의바랍니다.</b>
</p>
<?php echo(htmlspecialchars_decode('&lt;img id="test" width="0" height="0"&gt;'))?>
<script>
if(!document.getElementById("test")) {
alert("PHP가 실행되지 않았습니다. 내용을 로컬 파일로 전송한 것이 아니라 서버로 전송했는지 확인 해 주십시오.");
}
</script>
</body>
</html>

View File

@ -1,107 +0,0 @@
/**
* @use 간단 포토 업로드용으로 제작되었습니다.
* @author cielo
* @See nhn.husky.SE2M_Configuration
* @ 팝업 마크업은 SimplePhotoUpload.html과 SimplePhotoUpload_html5.html이 있습니다.
*/
nhn.husky.SE2M_AttachQuickPhoto = jindo.$Class({
name : "SE2M_AttachQuickPhoto",
$init : function(){},
$ON_MSG_APP_READY : function(){
this.oApp.exec("REGISTER_UI_EVENT", ["photo_attach", "click", "ATTACHPHOTO_OPEN_WINDOW"]);
},
$LOCAL_BEFORE_FIRST : function(sMsg){
if(!!this.oPopupMgr){ return; }
// Popup Manager에서 사용할 param
this.htPopupOption = {
oApp : this.oApp,
sName : this.name,
bScroll : false,
sProperties : "",
sUrl : ""
};
this.oPopupMgr = nhn.husky.PopUpManager.getInstance(this.oApp);
},
/**
* 포토 웹탑 오픈
*/
$ON_ATTACHPHOTO_OPEN_WINDOW : function(){
this.htPopupOption.sUrl = this.makePopupURL();
this.htPopupOption.sProperties = "left=0,top=0,width=403,height=359,scrollbars=yes,location=no,status=0,resizable=no";
this.oPopupWindow = this.oPopupMgr.openWindow(this.htPopupOption);
// 처음 로딩하고 IE에서 커서가 전혀 없는 경우
// 복수 업로드시에 순서가 바뀜
// [SMARTEDITORSUS-1698]
this.oApp.exec('FOCUS', [true]);
// --[SMARTEDITORSUS-1698]
return (!!this.oPopupWindow ? true : false);
},
/**
* 서비스별로 팝업에 parameter를 추가하여 URL을 생성하는 함수
* nhn.husky.SE2M_AttachQuickPhoto.prototype.makePopupURL로 덮어써서 사용하시면 .
*/
makePopupURL : function(){
var sPopupUrl = "./sample/photo_uploader/photo_uploader.html";
return sPopupUrl;
},
/**
* 팝업에서 호출되는 메세지.
*/
$ON_SET_PHOTO : function(aPhotoData){
var sContents,
aPhotoInfo,
htData;
if( !aPhotoData ){
return;
}
try{
sContents = "";
for(var i = 0; i <aPhotoData.length; i++){
htData = aPhotoData[i];
if(!htData.sAlign){
htData.sAlign = "";
}
aPhotoInfo = {
sName : htData.sFileName || "",
sOriginalImageURL : htData.sFileURL,
bNewLine : htData.bNewLine || false
};
sContents += this._getPhotoTag(aPhotoInfo);
}
this.oApp.exec("PASTE_HTML", [sContents]); // 위즐 첨부 파일 부분 확인
}catch(e){
// upload시 error발생에 대해서 skip함
return false;
}
},
/**
* @use 일반 포토 tag 생성
*/
_getPhotoTag : function(htPhotoInfo){
// id와 class는 썸네일과 연관이 많습니다. 수정시 썸네일 영역도 Test
var sTag = '<img src="{=sOriginalImageURL}" title="{=sName}" >';
if(htPhotoInfo.bNewLine){
sTag += '<br style="clear:both;">';
}
sTag = jindo.$Template(sTag).process(htPhotoInfo);
return sTag;
}
});

View File

@ -1,684 +0,0 @@
//변수 선언 및 초기화
var nImageInfoCnt = 0;
var htImageInfo = []; //image file정보 저장
var aResult = [];
var rFilter = /^(image\/bmp|image\/gif|image\/jpg|image\/jpeg|image\/png)$/i;
var rFilter2 = /^(bmp|gif|jpg|jpeg|png)$/i;
var nTotalSize = 0;
var nMaxImageSize = 10*1024*1024;
var nMaxTotalImageSize = 50*1024*1024;
var nMaxImageCount = 10;
var nImageFileCount = 0;
var bSupportDragAndDropAPI = false;
var oFileUploader;
var bAttachEvent = false;
//마크업에 따른 할당
var elContent= $("pop_content");
var elDropArea = jindo.$$.getSingle(".drag_area",elContent);
var elDropAreaUL = jindo.$$.getSingle(".lst_type",elContent);
var elCountTxtTxt = jindo.$$.getSingle("#imageCountTxt",elContent);
var elTotalSizeTxt = jindo.$$.getSingle("#totalSizeTxt",elContent);
var elTextGuide = $("guide_text");
var welUploadInputBox = $Element("uploadInputBox");
var oNavigator = jindo.$Agent().navigator();
//마크업-공통
var welBtnConfirm = $Element("btn_confirm"); //확인 버튼
var welBtnCancel= $Element("btn_cancel"); //취소 버튼
//진도로 랩핑된 element
var welTextGuide = $Element(elTextGuide);
var welDropArea = $Element(elDropArea);
var welDropAreaUL = $Element(elDropAreaUL);
var fnUploadImage = null;
//File API 지원 여부로 결정
function checkDragAndDropAPI(){
try{
if( !oNavigator.ie ){
if(!!oNavigator.safari && oNavigator.version <= 5){
bSupportDragAndDropAPI = false;
}else{
bSupportDragAndDropAPI = true;
}
} else {
bSupportDragAndDropAPI = false;
}
}catch(e){
bSupportDragAndDropAPI = false;
}
}
//--------------- html5 미지원 브라우저에서 (IE9 이하) ---------------
/**
* 이미지를 첨부 활성화된 버튼 상태
*/
function goStartMode(){
var sSrc = welBtnConfirm.attr("src")|| "";
if(sSrc.indexOf("btn_confirm2.png") < 0 ){
welBtnConfirm.attr("src","./img/btn_confirm2.png");
fnUploadImage.attach(welBtnConfirm.$value(), "click");
}
}
/**
* 이미지를 첨부 비활성화된 버튼 상태
* @return
*/
function goReadyMode(){
var sSrc = welBtnConfirm.attr("src")|| "";
if(sSrc.indexOf("btn_confirm2.png") >= 0 ){
fnUploadImage.detach(welBtnConfirm.$value(), "click");
welBtnConfirm.attr("src","./img/btn_confirm.png");
}
}
/**
* 일반 업로드
* @desc oFileUploader의 upload함수를 호출함.
*/
function generalUpload(){
oFileUploader.upload();
}
/**
* 이미지 첨부 안내 텍스트가 나오는 배경으로 '설정'하는 함수.
* @return
*/
function readyModeBG (){
var sClass = welTextGuide.className();
if(sClass.indexOf('nobg') >= 0){
welTextGuide.removeClass('nobg');
welTextGuide.className('bg');
}
}
/**
* 이미지 첨부 안내 텍스트가 나오는 배경을 '제거'하는 함수.
* @return
*/
function startModeBG (){
var sClass = welTextGuide.className();
if(sClass.indexOf('nobg') < 0){
welTextGuide.removeClass('bg');
welTextGuide.className('nobg');
}
}
//--------------------- html5 지원되는 브라우저에서 사용하는 함수 --------------------------
/**
* 팝업에 노출될 업로드 예정 사진의 .
* @param {Object} nCount 현재 업로드 예정인 사진 장수
* @param {Object} nVariable 삭제되는
*/
function updateViewCount (nCount, nVariable){
var nCnt = nCount + nVariable;
elCountTxtTxt.innerHTML = nCnt +"장";
nImageFileCount = nCnt;
return nCnt;
}
/**
* 팝업에 노출될 업로드될 사진 용량
*/
function updateViewTotalSize(){
var nViewTotalSize = Number(parseInt((nTotalSize || 0), 10) / (1024*1024));
elTotalSizeTxt.innerHTML = nViewTotalSize.toFixed(2) +"MB";
}
/**
* 이미지 전체 용량 재계산.
* @param {Object} sParentId
*/
function refreshTotalImageSize(sParentId){
var nDelImgSize = htImageInfo[sParentId].size;
if(nTotalSize - nDelImgSize > -1 ){
nTotalSize = nTotalSize - nDelImgSize;
}
}
/**
* hash table에서 이미지 정보 초기화.
* @param {Object} sParentId
*/
function removeImageInfo (sParentId){
//삭제된 이미지의 공간을 초기화 한다.
htImageInfo[sParentId] = null;
}
/**
* byte로 받은 이미지 용량을 화면에 표시를 위해 포맷팅
* @param {Object} nByte
*/
function setUnitString (nByte) {
var nImageSize;
var sUnit;
if(nByte < 0 ){
nByte = 0;
}
if( nByte < 1024) {
nImageSize = Number(nByte);
sUnit = 'B';
return nImageSize + sUnit;
} else if( nByte > (1024*1024)) {
nImageSize = Number(parseInt((nByte || 0), 10) / (1024*1024));
sUnit = 'MB';
return nImageSize.toFixed(2) + sUnit;
} else {
nImageSize = Number(parseInt((nByte || 0), 10) / 1024);
sUnit = 'KB';
return nImageSize.toFixed(0) + sUnit;
}
}
/**
* 화면 목록에 적당하게 이름을 잘라서 표시.
* @param {Object} sName 파일명
* @param {Object} nMaxLng 최대 길이
*/
function cuttingNameByLength (sName, nMaxLng) {
var sTemp, nIndex;
if(sName.length > nMaxLng){
nIndex = sName.indexOf(".");
sTemp = sName.substring(0,nMaxLng) + "..." + sName.substring(nIndex,sName.length) ;
} else {
sTemp = sName;
}
return sTemp;
}
/**
* Total Image Size를 체크해서 추가로 이미지를 넣을지 말지를 결정함.
* @param {Object} nByte
*/
function checkTotalImageSize(nByte){
if( nTotalSize + nByte < nMaxTotalImageSize){
nTotalSize = nTotalSize + nByte;
return false;
} else {
return true;
}
}
// 이벤트 핸들러 할당
function dragEnter(ev) {
ev.stopPropagation();
ev.preventDefault();
}
function dragExit(ev) {
ev.stopPropagation();
ev.preventDefault();
}
function dragOver(ev) {
ev.stopPropagation();
ev.preventDefault();
}
/**
* 드랍 영역에 사진을 떨구는 순간 발생하는 이벤트
* @param {Object} ev
*/
function drop(ev) {
ev.stopPropagation();
ev.preventDefault();
if (nImageFileCount >= 10){
alert("최대 10장까지만 등록할 수 있습니다.");
return;
}
if(typeof ev.dataTransfer.files == 'undefined'){
alert("HTML5를 지원하지 않는 브라우저입니다.");
}else{
//변수 선언
var wel,
files,
nCount,
sListTag = '';
//초기화
files = ev.dataTransfer.files;
nCount = files.length;
if (!!files && nCount === 0){
//파일이 아닌, 웹페이지에서 이미지를 드래서 놓는 경우.
alert("정상적인 첨부방식이 아닙니다.");
return ;
}
for (var i = 0, j = nImageFileCount ; i < nCount ; i++){
if (!rFilter.test(files[i].type)) {
alert("이미지파일 (jpg,gif,png,bmp)만 업로드 가능합니다.");
} else if(files[i].size > nMaxImageSize){
alert("이미지 용량이 10MB를 초과하여 등록할 수 없습니다.");
} else {
//제한된 수만 업로드 가능.
if ( j < nMaxImageCount ){
sListTag += addImage(files[i]);
//다음 사진을위한 셋팅
j = j+1;
nImageInfoCnt = nImageInfoCnt+1;
} else {
alert("최대 10장까지만 등록할 수 있습니다.");
break;
}
}
}
if(j > 0){
//배경 이미지 변경
startModeBG();
if ( sListTag.length > 1){
welDropAreaUL.prependHTML(sListTag);
}
//이미지 총사이즈 view update
updateViewTotalSize();
//이미치 총 수 view update
nImageFileCount = j;
updateViewCount(nImageFileCount, 0);
// 저장 버튼 활성화
goStartMode();
}else{
readyModeBG();
}
}
}
/**
* 이미지를 추가하기 위해서 file을 저장하고, 목록에 보여주기 위해서 string을 만드는 함수.
* @param ofile 한개의 이미지 파일
* @return
*/
function addImage(ofile){
//파일 사이즈
var ofile = ofile,
sFileSize = 0,
sFileName = "",
sLiTag = "",
bExceedLimitTotalSize = false,
aFileList = [];
sFileSize = setUnitString(ofile.size);
sFileName = cuttingNameByLength(ofile.name, 15);
bExceedLimitTotalSize = checkTotalImageSize(ofile.size);
if( !!bExceedLimitTotalSize ){
alert("전체 이미지 용량이 50MB를 초과하여 등록할 수 없습니다. \n\n (파일명 : "+sFileName+", 사이즈 : "+sFileSize+")");
} else {
//이미지 정보 저장
htImageInfo['img'+nImageInfoCnt] = ofile;
//List 마크업 생성하기
aFileList.push(' <li id="img'+nImageInfoCnt+'" class="imgLi"><span>'+ sFileName +'</span>');
aFileList.push(' <em>'+ sFileSize +'</em>');
aFileList.push(' <a onclick="delImage(\'img'+nImageInfoCnt+'\')"><img class="del_button" src="./img/btn_del.png" width="14" height="13" alt="첨부 사진 삭제"></a>');
aFileList.push(' </li> ');
sLiTag = aFileList.join(" ");
aFileList = [];
}
return sLiTag;
}
/**
* HTML5 DragAndDrop으로 사진을 추가하고, 확인버튼을 누른 경우에 동작한다.
* @return
*/
function html5Upload() {
var tempFile,
sUploadURL;
//sUploadURL= 'file_uploader_html5.php'; //upload URL
sUploadURL= 'file_uploader_html5.jsp'; //upload URL
//파일을 하나씩 보내고, 결과를 받음.
for(var j=0, k=0; j < nImageInfoCnt; j++) {
tempFile = htImageInfo['img'+j];
try{
if(!!tempFile){
//Ajax통신하는 부분. 파일과 업로더할 url을 전달한다.
callAjaxForHTML5(tempFile,sUploadURL);
k += 1;
}
}catch(e){}
tempFile = null;
}
}
function callAjaxForHTML5 (tempFile, sUploadURL){
var oAjax = jindo.$Ajax(sUploadURL, {
type: 'xhr',
method : "post",
onload : function(res){ // 요청이 완료되면 실행될 콜백 함수
var sResString = res._response.responseText;
if (res.readyState() == 4) {
if(sResString.indexOf("NOTALLOW_") > -1){
var sFileName = sResString.replace("NOTALLOW_", "");
alert("이미지 파일(jpg,gif,png,bmp)만 업로드 하실 수 있습니다. ("+sFileName+")");
}else{
//성공 시에 responseText를 가지고 array로 만드는 부분.
makeArrayFromString(res._response.responseText);
}
}
},
timeout : 3,
onerror : jindo.$Fn(onAjaxError, this).bind()
});
oAjax.header("contentType","multipart/form-data");
oAjax.header("file-name",encodeURIComponent(tempFile.name));
oAjax.header("file-size",tempFile.size);
oAjax.header("file-Type",tempFile.type);
oAjax.request(tempFile);
}
function makeArrayFromString(sResString){
var aTemp = [],
aSubTemp = [],
htTemp = {}
aResultleng = 0;
try{
if(!sResString || sResString.indexOf("sFileURL") < 0){
return ;
}
aTemp = sResString.split("&");
for (var i = 0; i < aTemp.length ; i++){
if( !!aTemp[i] && aTemp[i] != "" && aTemp[i].indexOf("=") > 0){
aSubTemp = aTemp[i].split("=");
htTemp[aSubTemp[0]] = aSubTemp[1];
}
}
}catch(e){}
aResultleng = aResult.length;
aResult[aResultleng] = htTemp;
if(aResult.length == nImageFileCount){
setPhotoToEditor(aResult);
aResult = null;
window.close();
}
}
/**
* 사진 삭제 시에 호출되는 함수
* @param {Object} sParentId
*/
function delImage (sParentId){
var elLi = jindo.$$.getSingle("#"+sParentId);
refreshTotalImageSize(sParentId);
updateViewTotalSize();
updateViewCount(nImageFileCount,-1);
//사진 file array에서 정보 삭제.
removeImageInfo(sParentId);
//해당 li삭제
$Element(elLi).leave();
//마지막 이미지인경우.
if(nImageFileCount === 0){
readyModeBG();
//사진 추가 버튼 비활성화
goReadyMode();
}
// drop 영역 이벤트 다시 활성화.
if(!bAttachEvent){
addEvent();
}
}
/**
* 이벤트 할당
*/
function addEvent() {
bAttachEvent = true;
elDropArea.addEventListener("dragenter", dragEnter, false);
elDropArea.addEventListener("dragexit", dragExit, false);
elDropArea.addEventListener("dragover", dragOver, false);
elDropArea.addEventListener("drop", drop, false);
}
function removeEvent(){
bAttachEvent = false;
elDropArea.removeEventListener("dragenter", dragEnter, false);
elDropArea.removeEventListener("dragexit", dragExit, false);
elDropArea.removeEventListener("dragover", dragOver, false);
elDropArea.removeEventListener("drop", drop, false);
}
/**
* Ajax 통신 error가 발생할 처리하는 함수입니다.
* @return
*/
function onAjaxError (){
alert("[가이드]사진 업로더할 서버URL셋팅이 필요합니다.-onAjaxError");
}
/**
* 이미지 업로드 시작
* 확인 버튼 클릭하면 호출되는 msg
*/
function uploadImage (e){
if(!bSupportDragAndDropAPI){
generalUpload();
}else{
html5Upload();
}
}
/**
* jindo에 파일 업로드 사용.(iframe에 Form을 Submit하여 리프레시없이 파일을 업로드하는 컴포넌트)
*/
function callFileUploader (){
oFileUploader = new jindo.FileUploader(jindo.$("uploadInputBox"),{
// sUrl : location.href.replace(/\/[^\/]*$/, '') + '/file_uploader.php', //샘플 URL입니다.
// sCallback : location.href.replace(/\/[^\/]*$/, '') + '/callback.html', //업로드 이후에 iframe이 redirect될 콜백페이지의 주소
sUrl : '/imageUpload.do', //파일업로드를 처리하는 페이지
sCallback : '/SE2/sample/photo_uploader/callback.html', //업로드 이후에 iframe이 redirect될 콜백페이지의 주소
sFiletype : "*.jpg;*.png;*.bmp;*.gif", //허용할 파일의 형식. ex) "*", "*.*", "*.jpg", 구분자(;)
sMsgNotAllowedExt : 'JPG, GIF, PNG, BMP 확장자만 가능합니다', //허용할 파일의 형식이 아닌경우에 띄워주는 경고창의 문구
bAutoUpload : false, //파일이 선택됨과 동시에 자동으로 업로드를 수행할지 여부 (upload 메소드 수행)
bAutoReset : true // 업로드한 직후에 파일폼을 리셋 시킬지 여부 (reset 메소드 수행)
}).attach({
select : function(oCustomEvent) {
//파일 선택이 완료되었을 때 발생
// oCustomEvent (이벤트 객체) = {
// sValue (String) 선택된 File Input의 값
// bAllowed (Boolean) 선택된 파일의 형식이 허용되는 형식인지 여부
// sMsgNotAllowedExt (String) 허용되지 않는 파일 형식인 경우 띄워줄 경고메세지
// }
// 선택된 파일의 형식이 허용되는 경우만 처리
if(oCustomEvent.bAllowed === true){
goStartMode();
}else{
goReadyMode();
oFileUploader.reset();
}
// bAllowed 값이 false인 경우 경고문구와 함께 alert 수행
// oCustomEvent.stop(); 수행시 bAllowed 가 false이더라도 alert이 수행되지 않음
},
success : function(oCustomEvent) {
// alert("success");
// 업로드가 성공적으로 완료되었을 때 발생
// oCustomEvent(이벤트 객체) = {
// htResult (Object) 서버에서 전달해주는 결과 객체 (서버 설정에 따라 유동적으로 선택가능)
// }
var aResult = [];
aResult[0] = oCustomEvent.htResult;
setPhotoToEditor(aResult);
//버튼 비활성화
goReadyMode();
oFileUploader.reset();
window.close();
},
error : function(oCustomEvent) {
//업로드가 실패했을 때 발생
//oCustomEvent(이벤트 객체) = {
// htResult : { (Object) 서버에서 전달해주는 결과 객체. 에러발생시 errstr 프로퍼티를 반드시 포함하도록 서버 응답을 설정하여야한다.
// errstr : (String) 에러메시지
// }
//}
//var wel = jindo.$Element("info");
//wel.html(oCustomEvent.htResult.errstr);
alert(oCustomEvent.htResult.errstr);
}
});
}
/**
* 페이지 닫기 버튼 클릭
*/
function closeWindow(){
if(bSupportDragAndDropAPI){
removeEvent();
}
window.close();
}
window.onload = function(){
checkDragAndDropAPI();
if(bSupportDragAndDropAPI){
$Element("pop_container2").hide();
$Element("pop_container").show();
welTextGuide.removeClass("nobg");
welTextGuide.className("bg");
addEvent();
} else {
$Element("pop_container").hide();
$Element("pop_container2").show();
callFileUploader();
}
fnUploadImage = $Fn(uploadImage,this);
$Fn(closeWindow,this).attach(welBtnCancel.$value(), "click");
};
/**
* 서버로부터 받은 데이타를 에디터에 전달하고 창을 닫음.
* @parameter aFileInfo [{},{},...]
* @ex aFileInfo = [
* {
sFileName : "nmms_215646753.gif",
sFileURL :"http://static.naver.net/www/u/2010/0611/nmms_215646753.gif",
bNewLine : true
},
{
sFileName : "btn_sch_over.gif",
sFileURL :"http://static1.naver.net/w9/btn_sch_over.gif",
bNewLine : true
}
* ]
*/
function setPhotoToEditor(oFileInfo){
if (!!opener && !!opener.nhn && !!opener.nhn.husky && !!opener.nhn.husky.PopUpManager) {
//스마트 에디터 플러그인을 통해서 넣는 방법 (oFileInfo는 Array)
opener.nhn.husky.PopUpManager.setCallback(window, 'SET_PHOTO', [oFileInfo]);
//본문에 바로 tag를 넣는 방법 (oFileInfo는 String으로 <img src=....> )
//opener.nhn.husky.PopUpManager.setCallback(window, 'PASTE_HTML', [oFileInfo]);
}
}
// 2012.05 현재] jindo.$Ajax.prototype.request에서 file과 form을 지원하지 안함.
jindo.$Ajax.prototype.request = function(oData) {
this._status++;
var t = this;
var req = this._request;
var opt = this._options;
var data, v,a = [], data = "";
var _timer = null;
var url = this._url;
this._is_abort = false;
if( opt.postBody && opt.type.toUpperCase()=="XHR" && opt.method.toUpperCase()!="GET"){
if(typeof oData == 'string'){
data = oData;
}else{
data = jindo.$Json(oData).toString();
}
}else if (typeof oData == "undefined" || !oData) {
data = null;
} else {
data = oData;
}
req.open(opt.method.toUpperCase(), url, opt.async);
if (opt.sendheader) {
if(!this._headers["Content-Type"]){
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
}
req.setRequestHeader("charset", "utf-8");
for (var x in this._headers) {
if(this._headers.hasOwnProperty(x)){
if (typeof this._headers[x] == "function")
continue;
req.setRequestHeader(x, String(this._headers[x]));
}
}
}
var navi = navigator.userAgent;
if(req.addEventListener&&!(navi.indexOf("Opera") > -1)&&!(navi.indexOf("MSIE") > -1)){
/*
* opera 10.60에서 XMLHttpRequest에 addEventListener기 추가되었지만 정상적으로 동작하지 않아 opera는 무조건 dom1방식으로 지원함.
* IE9에서도 opera와 같은 문제가 있음.
*/
if(this._loadFunc){ req.removeEventListener("load", this._loadFunc, false); }
this._loadFunc = function(rq){
clearTimeout(_timer);
_timer = undefined;
t._onload(rq);
}
req.addEventListener("load", this._loadFunc, false);
}else{
if (typeof req.onload != "undefined") {
req.onload = function(rq){
if(req.readyState == 4 && !t._is_abort){
clearTimeout(_timer);
_timer = undefined;
t._onload(rq);
}
};
} else {
/*
* IE6에서는 onreadystatechange가 동기적으로 실행되어 timeout이벤트가 발생안됨.
* 그래서 interval로 체크하여 timeout이벤트가 정상적으로 발생되도록 수정. 비동기 방식일때만
*/
if(window.navigator.userAgent.match(/(?:MSIE) ([0-9.]+)/)[1]==6&&opt.async){
var onreadystatechange = function(rq){
if(req.readyState == 4 && !t._is_abort){
if(_timer){
clearTimeout(_timer);
_timer = undefined;
}
t._onload(rq);
clearInterval(t._interval);
t._interval = undefined;
}
};
this._interval = setInterval(onreadystatechange,300);
}else{
req.onreadystatechange = function(rq){
if(req.readyState == 4){
clearTimeout(_timer);
_timer = undefined;
t._onload(rq);
}
};
}
}
}
req.send(data);
return this;
};

View File

@ -1,31 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>FileUploader Callback</title>
</head>
<body>
<script type="text/javascript">
// alert("callback");
// document.domain 설정
try { document.domain = "http://localhost"; } catch(e) {}
// execute callback script
var sUrl = document.location.search.substr(1);
if (sUrl != "blank") {
var oParameter = {}; // query array
sUrl.replace(/([^=]+)=([^&]*)(&|$)/g, function(){
oParameter[arguments[1]] = arguments[2];
return "";
});
if ((oParameter.errstr || '').length) { // on error
(parent.jindo.FileUploader._oCallback[oParameter.callback_func+'_error'])(oParameter);
} else {
(parent.jindo.FileUploader._oCallback[oParameter.callback_func+'_success'])(oParameter);
}
}
</script>
</body>
</html>

View File

@ -1,96 +0,0 @@
<%--------------------------------------------------------------------------------
* 화면명 : Smart Editor 2.8 에디터 - 싱글 파일 업로드 처리
* 파일명 : /SE2/sample/photo_uploader/file_uploader.jsp
--------------------------------------------------------------------------------%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="java.util.List"%>
<%@ page import="java.util.UUID"%>
<%@ page import="java.io.File"%>
<%@ page import="java.io.FileOutputStream"%>
<%@ page import="java.io.InputStream"%>
<%@ page import="java.io.OutputStream"%>
<%@ page import="org.apache.commons.fileupload.FileItem"%>
<%@ page import="org.apache.commons.fileupload.disk.DiskFileItemFactory"%>
<%@ page import="org.apache.commons.fileupload.servlet.ServletFileUpload"%>
<%
// 로컬경로에 파일 저장하기 ============================================
String return1 = "";
String return2 = "";
String return3 = "";
String name = "";
// multipart로 전송되었는가 체크
if(ServletFileUpload.isMultipartContent(request)) {
ServletFileUpload uploadHandler = new ServletFileUpload(new DiskFileItemFactory());
// UTF-8 인코딩 설정
uploadHandler.setHeaderEncoding("UTF-8");
List<FileItem> items = uploadHandler.parseRequest(request);
// 각 필드태그들을 FOR문을 이용하여 비교를 합니다.
for(FileItem item : items) {
if(item.getFieldName().equals("callback")) {
return1 = item.getString("UTF-8");
} else if(item.getFieldName().equals("callback_func")) {
return2 = "?callback_func="+item.getString("UTF-8");
} else if(item.getFieldName().equals("Filedata")) {
// FILE 태그가 1개이상일 경우
if(item.getSize() > 0) {
// 확장자
String ext = item.getName().substring(item.getName().lastIndexOf(".")+1);
// 파일 기본경로
String defaultPath = request.getServletContext().getRealPath("/");
// 파일 기본경로 _ 상세경로
String path = defaultPath + "upload" + File.separator;
File file = new File(path);
// 디렉토리 존재하지 않을경우 디렉토리 생성
if(!file.exists()) {
file.mkdirs();
}
// 서버에 업로드 할 파일명(한글문제로 인해 원본파일은 올리지 않는것이 좋음)
String realname = UUID.randomUUID().toString() + "." + ext;
///////////////// 서버에 파일쓰기 /////////////////
InputStream is = item.getInputStream();
OutputStream os=new FileOutputStream(path + realname);
int numRead;
byte b[] = new byte[(int)item.getSize()];
while((numRead = is.read(b,0,b.length)) != -1) {
os.write(b,0,numRead);
}
if(is != null) is.close();
os.flush();
os.close();
System.out.println("path : "+path);
System.out.println("realname : "+realname);
// 파일 삭제
// File f1 = new File(path, realname);
// if (!f1.isDirectory()) {
// if(!f1.delete()) {
// System.out.println("File 삭제 오류!");
// }
// }
///////////////// 서버에 파일쓰기 /////////////////
return3 += "&bNewLine=true&sFileName="+name+"&sFileURL=/upload/"+realname;
} else {
return3 += "&errstr=error";
}
}
}
}
response.sendRedirect(return1+return2+return3);
// ./로컬경로에 파일 저장하기 ============================================
%>

View File

@ -1,37 +0,0 @@
<?php
// default redirection
$url = $_REQUEST["callback"].'?callback_func='.$_REQUEST["callback_func"];
$bSuccessUpload = is_uploaded_file($_FILES['Filedata']['tmp_name']);
// SUCCESSFUL
if(bSuccessUpload) {
$tmp_name = $_FILES['Filedata']['tmp_name'];
$name = $_FILES['Filedata']['name'];
$filename_ext = strtolower(array_pop(explode('.',$name)));
$allow_file = array("jpg", "png", "bmp", "gif");
if(!in_array($filename_ext, $allow_file)) {
$url .= '&errstr='.$name;
} else {
$uploadDir = '../../upload/';
if(!is_dir($uploadDir)){
mkdir($uploadDir, 0777);
}
$newPath = $uploadDir.urlencode($_FILES['Filedata']['name']);
@move_uploaded_file($tmp_name, $newPath);
$url .= "&bNewLine=true";
$url .= "&sFileName=".urlencode(urlencode($name));
$url .= "&sFileURL=upload/".urlencode(urlencode($name));
}
}
// FAILED
else {
$url .= '&errstr=error';
}
header('Location: '. $url);
?>

View File

@ -1,69 +0,0 @@
<%--------------------------------------------------------------------------------
* 화면명 : Smart Editor 2.8 에디터 - 다중 파일 업로드 처리
* 파일명 : /SE2/sample/photo_uploader/file_uploader_html5.jsp
--------------------------------------------------------------------------------%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="java.util.List"%>
<%@ page import="java.util.UUID"%>
<%@ page import="java.io.File"%>
<%@ page import="java.io.FileOutputStream"%>
<%@ page import="java.io.InputStream"%>
<%@ page import="java.io.OutputStream"%>
<%@ page import="org.apache.commons.fileupload.FileItem"%>
<%@ page import="org.apache.commons.fileupload.disk.DiskFileItemFactory"%>
<%@ page import="org.apache.commons.fileupload.servlet.ServletFileUpload"%>
<%
// 로컬경로에 파일 저장하기 ============================================
String sFileInfo = "";
// 파일명 - 싱글파일업로드와 다르게 멀티파일업로드는 HEADER로 넘어옴
String name = request.getHeader("file-name");
// 확장자
String ext = name.substring(name.lastIndexOf(".")+1);
// 파일 기본경로
String defaultPath = request.getServletContext().getRealPath("/");
// 파일 기본경로 _ 상세경로
String path = defaultPath + "upload" + File.separator;
File file = new File(path);
if(!file.exists()) {
file.mkdirs();
}
String realname = UUID.randomUUID().toString() + "." + ext;
InputStream is = request.getInputStream();
OutputStream os = new FileOutputStream(path + realname);
int numRead;
// 파일쓰기
byte b[] = new byte[Integer.parseInt(request.getHeader("file-size"))];
while((numRead = is.read(b,0,b.length)) != -1) {
os.write(b,0,numRead);
}
if(is != null) {
is.close();
}
os.flush();
os.close();
System.out.println("path : "+path);
System.out.println("realname : "+realname);
// 파일 삭제
// File f1 = new File(path, realname);
// if (!f1.isDirectory()) {
// if(!f1.delete()) {
// System.out.println("File 삭제 오류!");
// }
// }
sFileInfo += "&bNewLine=true&sFileName="+ name+"&sFileURL="+"/upload/"+realname;
out.println(sFileInfo);
// ./로컬경로에 파일 저장하기 ============================================
%>

View File

@ -1,38 +0,0 @@
<?php
$sFileInfo = '';
$headers = array();
foreach($_SERVER as $k => $v) {
if(substr($k, 0, 9) == "HTTP_FILE") {
$k = substr(strtolower($k), 5);
$headers[$k] = $v;
}
}
$file = new stdClass;
$file->name = str_replace("\0", "", rawurldecode($headers['file_name']));
$file->size = $headers['file_size'];
$file->content = file_get_contents("php://input");
$filename_ext = strtolower(array_pop(explode('.',$file->name)));
$allow_file = array("jpg", "png", "bmp", "gif");
if(!in_array($filename_ext, $allow_file)) {
echo "NOTALLOW_".$file->name;
} else {
$uploadDir = '../../upload/';
if(!is_dir($uploadDir)){
mkdir($uploadDir, 0777);
}
$newPath = $uploadDir.iconv("utf-8", "cp949", $file->name);
if(file_put_contents($newPath, $file->content)) {
$sFileInfo .= "&bNewLine=true";
$sFileInfo .= "&sFileName=".$file->name;
$sFileInfo .= "&sFileURL=upload/".$file->name;
}
echo $sFileInfo;
}
?>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 506 B

View File

@ -1,390 +0,0 @@
/**
* Jindo Component
* @version 1.0.3
* NHN_Library:Jindo_Component-1.0.3;JavaScript Components for Jindo;
* @include Component, UIComponent, FileUploader
*/
jindo.Component = jindo.$Class({
_htEventHandler: null,
_htOption: null,
$init: function () {
var aInstance = this.constructor.getInstance();
aInstance.push(this);
this._htEventHandler = {};
this._htOption = {};
this._htOption._htSetter = {};
},
option: function (sName, vValue) {
switch (typeof sName) {
case "undefined":
return this._htOption;
case "string":
if (typeof vValue != "undefined") {
if (sName == "htCustomEventHandler") {
if (typeof this._htOption[sName] == "undefined") {
this.attach(vValue);
} else {
return this;
}
}
this._htOption[sName] = vValue;
if (typeof this._htOption._htSetter[sName] == "function") {
this._htOption._htSetter[sName](vValue);
}
} else {
return this._htOption[sName];
}
break;
case "object":
for (var sKey in sName) {
if (sKey == "htCustomEventHandler") {
if (typeof this._htOption[sKey] == "undefined") {
this.attach(sName[sKey]);
} else {
continue;
}
}
this._htOption[sKey] = sName[sKey];
if (typeof this._htOption._htSetter[sKey] == "function") {
this._htOption._htSetter[sKey](sName[sKey]);
}
}
break;
}
return this;
},
optionSetter: function (sName, fSetter) {
switch (typeof sName) {
case "undefined":
return this._htOption._htSetter;
case "string":
if (typeof fSetter != "undefined") {
this._htOption._htSetter[sName] = jindo.$Fn(fSetter, this).bind();
} else {
return this._htOption._htSetter[sName];
}
break;
case "object":
for (var sKey in sName) {
this._htOption._htSetter[sKey] = jindo.$Fn(sName[sKey], this).bind();
}
break;
}
return this;
},
fireEvent: function (sEvent, oEvent) {
oEvent = oEvent || {};
var fInlineHandler = this['on' + sEvent],
aHandlerList = this._htEventHandler[sEvent] || [],
bHasInlineHandler = typeof fInlineHandler == "function",
bHasHandlerList = aHandlerList.length > 0;
if (!bHasInlineHandler && !bHasHandlerList) {
return true;
}
aHandlerList = aHandlerList.concat();
oEvent.sType = sEvent;
if (typeof oEvent._aExtend == 'undefined') {
oEvent._aExtend = [];
oEvent.stop = function () {
if (oEvent._aExtend.length > 0) {
oEvent._aExtend[oEvent._aExtend.length - 1].bCanceled = true;
}
};
}
oEvent._aExtend.push({
sType: sEvent,
bCanceled: false
});
var aArg = [oEvent],
i, nLen;
for (i = 2, nLen = arguments.length; i < nLen; i++) {
aArg.push(arguments[i]);
}
if (bHasInlineHandler) {
fInlineHandler.apply(this, aArg);
}
if (bHasHandlerList) {
var fHandler;
for (i = 0, fHandler;
(fHandler = aHandlerList[i]); i++) {
fHandler.apply(this, aArg);
}
}
return !oEvent._aExtend.pop().bCanceled;
},
attach: function (sEvent, fHandlerToAttach) {
if (arguments.length == 1) {
jindo.$H(arguments[0]).forEach(jindo.$Fn(function (fHandler, sEvent) {
this.attach(sEvent, fHandler);
}, this).bind());
return this;
}
var aHandler = this._htEventHandler[sEvent];
if (typeof aHandler == 'undefined') {
aHandler = this._htEventHandler[sEvent] = [];
}
aHandler.push(fHandlerToAttach);
return this;
},
detach: function (sEvent, fHandlerToDetach) {
if (arguments.length == 1) {
jindo.$H(arguments[0]).forEach(jindo.$Fn(function (fHandler, sEvent) {
this.detach(sEvent, fHandler);
}, this).bind());
return this;
}
var aHandler = this._htEventHandler[sEvent];
if (aHandler) {
for (var i = 0, fHandler;
(fHandler = aHandler[i]); i++) {
if (fHandler === fHandlerToDetach) {
aHandler = aHandler.splice(i, 1);
break;
}
}
}
return this;
},
detachAll: function (sEvent) {
var aHandler = this._htEventHandler;
if (arguments.length) {
if (typeof aHandler[sEvent] == 'undefined') {
return this;
}
delete aHandler[sEvent];
return this;
}
for (var o in aHandler) {
delete aHandler[o];
}
return this;
}
});
jindo.Component.factory = function (aObject, htOption) {
var aReturn = [],
oInstance;
if (typeof htOption == "undefined") {
htOption = {};
}
for (var i = 0, el;
(el = aObject[i]); i++) {
oInstance = new this(el, htOption);
aReturn[aReturn.length] = oInstance;
}
return aReturn;
};
jindo.Component.getInstance = function () {
if (typeof this._aInstance == "undefined") {
this._aInstance = [];
}
return this._aInstance;
};
jindo.UIComponent = jindo.$Class({
$init: function () {
this._bIsActivating = false;
},
isActivating: function () {
return this._bIsActivating;
},
activate: function () {
if (this.isActivating()) {
return this;
}
this._bIsActivating = true;
if (arguments.length > 0) {
this._onActivate.apply(this, arguments);
} else {
this._onActivate();
}
return this;
},
deactivate: function () {
if (!this.isActivating()) {
return this;
}
this._bIsActivating = false;
if (arguments.length > 0) {
this._onDeactivate.apply(this, arguments);
} else {
this._onDeactivate();
}
return this;
}
}).extend(jindo.Component);
jindo.FileUploader = jindo.$Class({
_bIsActivating: false,
_aHiddenInput: [],
$init: function (elFileSelect, htOption) {
var htDefaultOption = {
sUrl: '',
sCallback: '',
htData: {},
sFiletype: '*',
sMsgNotAllowedExt: "업로드가 허용되지 않는 파일형식입니다",
bAutoUpload: false,
bAutoReset: true,
bActivateOnload: true
};
this.option(htDefaultOption);
this.option(htOption || {});
this._el = jindo.$(elFileSelect);
this._wel = jindo.$Element(this._el);
this._elForm = this._el.form;
this._aHiddenInput = [];
this.constructor._oCallback = {};
this._wfChange = jindo.$Fn(this._onFileSelectChange, this);
if (this.option("bActivateOnload")) {
this.activate();
}
},
_appendIframe: function () {
var sIframeName = 'tmpFrame_' + this._makeUniqueId();
this._welIframe = jindo.$Element(jindo.$('<iframe name="' + sIframeName + '" src="' + this.option("sCallback") + '?blank">')).css({
width: '10px',
border: '2px',
height: '10px',
left: '10px',
top: '10px'
});
document.body.appendChild(this._welIframe.$value());
},
_removeIframe: function () {
this._welIframe.leave();
},
getBaseElement: function () {
return this.getFileSelect();
},
getFileSelect: function () {
return this._el;
},
getFormElement: function () {
return this._elForm;
},
upload: function () {
this._appendIframe();
var elForm = this.getFormElement(),
welForm = jindo.$Element(elForm),
sIframeName = this._welIframe.attr("name"),
sFunctionName = sIframeName + '_func',
sAction = this.option("sUrl");
welForm.attr({
target: sIframeName,
action: sAction
});
this._aHiddenInput.push(this._createElement('input', {
'type': 'hidden',
'name': 'callback',
'value': this.option("sCallback")
}));
this._aHiddenInput.push(this._createElement('input', {
'type': 'hidden',
'name': 'callback_func',
'value': sFunctionName
}));
for (var k in this.option("htData")) {
this._aHiddenInput.push(this._createElement('input', {
'type': 'hidden',
'name': k,
'value': this.option("htData")[k]
}));
}
for (var i = 0; i < this._aHiddenInput.length; i++) {
elForm.appendChild(this._aHiddenInput[i]);
}
this.constructor._oCallback[sFunctionName + '_success'] = jindo.$Fn(function (oParameter) {
this.fireEvent("success", {
htResult: oParameter
});
delete this.constructor._oCallback[oParameter.callback_func + '_success'];
delete this.constructor._oCallback[oParameter.callback_func + '_error'];
for (var i = 0; i < this._aHiddenInput.length; i++) {
jindo.$Element(this._aHiddenInput[i]).leave();
}
this._aHiddenInput.length = 0;
this._removeIframe();
}, this).bind();
this.constructor._oCallback[sFunctionName + '_error'] = jindo.$Fn(function (oParameter) {
this.fireEvent("error", {
htResult: oParameter
});
delete this.constructor._oCallback[oParameter.callback_func + '_success'];
delete this.constructor._oCallback[oParameter.callback_func + '_error'];
for (var i = 0; i < this._aHiddenInput.length; i++) {
jindo.$Element(this._aHiddenInput[i]).leave();
}
this._aHiddenInput.length = 0;
this._removeIframe();
}, this).bind();
elForm.submit();
if (this.option("bAutoReset")) {
this.reset();
}
},
reset: function () {
var elWrapForm = jindo.$("<form>");
this._wel.wrap(elWrapForm);
elWrapForm.reset();
jindo.$Element(elWrapForm).replace(this._el);
var elForm = this.getFormElement(),
welForm = jindo.$Element(elForm);
welForm.attr({
target: this._sPrevTarget,
action: this._sAction
});
return this;
},
_onActivate: function () {
var elForm = this.getFormElement(),
welForm = jindo.$Element(elForm);
this._sPrevTarget = welForm.attr("target");
this._sAction = welForm.attr("action");
this._el.value = "";
this._wfChange.attach(this._el, "change");
},
_onDeactivate: function () {
this._wfChange.detach(this._el, "change");
},
_makeUniqueId: function () {
return new Date().getMilliseconds() + Math.floor(Math.random() * 100000);
},
_createElement: function (name, attributes) {
var el = jindo.$("<" + name + ">");
var wel = jindo.$Element(el);
for (var k in attributes) {
wel.attr(k, attributes[k]);
}
return el;
},
_checkExtension: function (sFile) {
var aType = this.option("sFiletype").split(';');
for (var i = 0, sType; i < aType.length; i++) {
sType = (aType[i] == "*.*") ? "*" : aType[i];
sType = sType.replace(/^\s+|\s+$/, '');
sType = sType.replace(/\./g, '\\.');
sType = sType.replace(/\*/g, '[^\\\/]+');
if ((new RegExp(sType + '$', 'gi')).test(sFile)) {
return true;
}
}
return false;
},
_onFileSelectChange: function (we) {
var sValue = we.element.value,
bAllowed = this._checkExtension(sValue),
htParam = {
sValue: sValue,
bAllowed: bAllowed,
sMsgNotAllowedExt: this.option("sMsgNotAllowedExt")
};
if (sValue.length && this.fireEvent("select", htParam)) {
if (bAllowed) {
if (this.option("bAutoUpload")) {
this.upload();
}
} else {
alert(htParam.sMsgNotAllowedExt);
}
}
}
}).extend(jindo.UIComponent);

File diff suppressed because it is too large Load Diff

View File

@ -1,101 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="Content-Script-Type" content="text/javascript">
<meta http-equiv="Content-Style-Type" content="text/css">
<title>사진 첨부하기 :: SmartEditor2</title>
<style type="text/css">
/* NHN Web Standard 1Team JJS 120106 */
/* Common */
body,p,h1,h2,h3,h4,h5,h6,ul,ol,li,dl,dt,dd,table,th,td,form,fieldset,legend,input,textarea,button,select{margin:0;padding:0}
body,input,textarea,select,button,table{font-family:'돋움',Dotum,Helvetica,sans-serif;font-size:12px}
img,fieldset{border:0}
ul,ol{list-style:none}
em,address{font-style:normal}
a{text-decoration:none}
a:hover,a:active,a:focus{text-decoration:underline}
/* Contents */
.blind{visibility:hidden;position:absolute;line-height:0}
#pop_wrap{width:383px}
#pop_header{height:26px;padding:14px 0 0 20px;border-bottom:1px solid #ededeb;background:#f4f4f3}
.pop_container{padding:11px 20px 0}
#pop_footer{margin:21px 20px 0;padding:10px 0 16px;border-top:1px solid #e5e5e5;text-align:center}
h1{color:#333;font-size:14px;letter-spacing:-1px}
.btn_area{word-spacing:2px}
.pop_container .drag_area{overflow:hidden;overflow-y:auto;position:relative;width:341px;height:129px;margin-top:4px;border:1px solid #eceff2}
.pop_container .drag_area .bg{display:block;position:absolute;top:0;left:0;width:341px;height:129px;background:#fdfdfd url(./img/bg_drag_image.png) 0 0 no-repeat}
.pop_container .nobg{background:none}
.pop_container .bar{color:#e0e0e0}
.pop_container .lst_type li{overflow:hidden;position:relative;padding:7px 0 6px 8px;border-bottom:1px solid #f4f4f4;vertical-align:top}
.pop_container :root .lst_type li{padding:6px 0 5px 8px}
.pop_container .lst_type li span{float:left;color:#222}
.pop_container .lst_type li em{float:right;margin-top:1px;padding-right:22px;color:#a1a1a1;font-size:11px}
.pop_container .lst_type li a{position:absolute;top:6px;right:5px}
.pop_container .dsc{margin-top:6px;color:#666;line-height:18px}
.pop_container .dsc_v1{margin-top:12px}
.pop_container .dsc em{color:#13b72a}
.pop_container2{padding:46px 60px 20px}
.pop_container2 .dsc{margin-top:6px;color:#666;line-height:18px}
.pop_container2 .dsc strong{color:#13b72a}
.upload{margin:0 4px 0 0;_margin:0;padding:6px 0 4px 6px;border:solid 1px #d5d5d5;color:#a1a1a1;font-size:12px;border-right-color:#efefef;border-bottom-color:#efefef;length:300px;}
:root .upload{padding:6px 0 2px 6px;}
</style>
</head>
<body>
<div id="pop_wrap">
<!-- header -->
<div id="pop_header">
<h1>사진 첨부하기</h1>
</div>
<!-- //header -->
<!-- container -->
<!-- [D] HTML5인 경우 pop_container 클래스와 하위 HTML 적용
그밖의 경우 pop_container2 클래스와 하위 HTML 적용 -->
<div id="pop_container2" class="pop_container2">
<!-- content -->
<!-- <form id="editor_upimage" name="editor_upimage" action="FileUploader.php" method="post" enctype="multipart/form-data" onSubmit="return false;"> -->
<form id="editor_upimage" name="editor_upimage" method="post" enctype="multipart/form-data" onSubmit="return false;">
<div id="pop_content2">
<input type="file" class="upload" id="uploadInputBox" name="Filedata">
<p class="dsc" id="info"><strong>10MB</strong>이하의 이미지 파일만 등록할 수 있습니다.<br>(JPG, GIF, PNG, BMP)</p>
</div>
</form>
<!-- //content -->
</div>
<div id="pop_container" class="pop_container" style="display:none;">
<!-- content -->
<div id="pop_content">
<p class="dsc"><em id="imageCountTxt">0장</em>/10장 <span class="bar">|</span> <em id="totalSizeTxt">0MB</em>/50MB</p>
<!-- [D] 첨부 이미지 여부에 따른 Class 변화
첨부 이미지가 있는 경우 : em에 "bg" 클래스 적용 //첨부 이미지가 없는 경우 : em에 "nobg" 클래스 적용 -->
<div class="drag_area" id="drag_area">
<ul class="lst_type" >
</ul>
<em class="blind">마우스로 드래그해서 이미지를 추가해주세요.</em><span id="guide_text" class="bg"></span>
</div>
<div style="display:none;" id="divImageList"></div>
<p class="dsc dsc_v1"><em>한 장당 10MB, 1회에 50MB까지, 10개</em>의 이미지 파일을<br>등록할 수 있습니다. (JPG, GIF, PNG, BMP)</p>
</div>
<!-- //content -->
</div>
<!-- //container -->
<!-- footer -->
<div id="pop_footer">
<div class="btn_area">
<a href="#"><img src="./img/btn_confirm.png" width="49" height="28" alt="확인" id="btn_confirm"></a>
<a href="#"><img src="./img/btn_cancel.png" width="48" height="28" alt="취소" id="btn_cancel"></a>
</div>
</div>
<!-- //footer -->
</div>
<script type="text/javascript" src="jindo.min.js" charset="utf-8"></script>
<script type="text/javascript" src="jindo.fileuploader.js" charset="utf-8"></script>
<script type="text/javascript" src="attach_photo.js" charset="utf-8"></script>
</body>
</html>

View File

@ -1,8 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>Smart Editor&#8482; WYSIWYG Mode</title>
</head>
<body class="se2_inputarea" style="height:0;-webkit-nbsp-mode:normal"></body>
</html>

View File

@ -1,9 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7">
<title>Smart Editor&#8482; WYSIWYG Mode</title>
</head>
<body class="se2_inputarea" style="height:0"></body>
</html>

Binary file not shown.

Binary file not shown.

View File

@ -1,56 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:task="http://www.springframework.org/schema/task"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-3.0.xsd
">
<context:component-scan base-package="com.pms"></context:component-scan>
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
<value>classpath:com/pms/message/message</value>
<value>classpath:com/pms/message/common</value>
</list>
</property>
<property name="cacheSeconds">
<value>60</value>
</property>
<property name="defaultEncoding" value="UTF-8"/>
</bean>
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
<property name="defaultLocale" value="ko"/>
</bean>
<bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="lang" />
</bean>
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/view"/>
<property name="suffix" value=".jsp"/>
<property name="order" value="1"/>
</bean>
<bean id="jacksonMessageConverter" class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"></bean>
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="messageConverters">
<list>
<ref bean="jacksonMessageConverter"/>
</list>
</property>
</bean>
<task:scheduler id="gsScheduler" pool-size="10" />
<task:annotation-driven scheduler="gsScheduler" />
</beans>

Binary file not shown.

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