Compare commits
400 Commits
regacy-del
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
1ee946d712 | |
|
|
db31b02180 | |
|
|
ee1760da2f | |
|
|
7ab05aea72 | |
|
|
ea6c5ac43c | |
|
|
ea0123d6cc | |
|
|
93eaf59966 | |
|
|
c56f434ff1 | |
|
|
536982dd71 | |
|
|
0e8c68a9ff | |
|
|
bfd97c9717 | |
|
|
a81cb7ca19 | |
|
|
12a8290873 | |
|
|
7a9a705f19 | |
|
|
85bf4882a8 | |
|
|
b2b0b575df | |
|
|
f7bd2f6fa3 | |
|
|
7e2ae4335e | |
|
|
d58131d88d | |
|
|
1917b7253d | |
|
|
9f9b130738 | |
|
|
91c9dda6ae | |
|
|
d43f0821ed | |
|
|
4b8f2b7839 | |
|
|
4f639dec34 | |
|
|
772514c270 | |
|
|
6f7e2b1b0c | |
|
|
b9080d03f6 | |
|
|
8d0f2dbb27 | |
|
|
a69b135b65 | |
|
|
fcb122c58b | |
|
|
e11a7b1237 | |
|
|
366cfcde60 | |
|
|
5b6b4be73c | |
|
|
b40f6c28dc | |
|
|
d31568b1bd | |
|
|
3a3e4e8926 | |
|
|
818cc80514 | |
|
|
58a62a6d4e | |
|
|
b950a102f1 | |
|
|
a6d6b03195 | |
|
|
359e333370 | |
|
|
95c42a971c | |
|
|
ac2da7a1d7 | |
|
|
4da5d10f09 | |
|
|
5f3b144b12 | |
|
|
b9c0a0f243 | |
|
|
a6c0ab5664 | |
|
|
f97edad1ea | |
|
|
f6a2668bdc | |
|
|
93d9df3e5a | |
|
|
ea8b4ce5dc | |
|
|
96637a9cb6 | |
|
|
e5abd93600 | |
|
|
cc44f714c6 | |
|
|
9b153d85af | |
|
|
37d93d82b1 | |
|
|
459777d5f0 | |
|
|
b4d5367e2b | |
|
|
15e22ba401 | |
|
|
ce5c2426b5 | |
|
|
c22b468599 | |
|
|
6a30038785 | |
|
|
89af350935 | |
|
|
d9d18c1922 | |
|
|
0d71e79c54 | |
|
|
6c9e35e8b2 | |
|
|
94a541fc9c | |
|
|
d7ef26d679 | |
|
|
2e8300bbf5 | |
|
|
65d5392c26 | |
|
|
f12fca46be | |
|
|
f04d224b09 | |
|
|
ce8b4ed688 | |
|
|
e3ae8d273c | |
|
|
f697e1e897 | |
|
|
fd5c61b12a | |
|
|
7ad17065f0 | |
|
|
e2d88f01e3 | |
|
|
e16d76936b | |
|
|
a8ad26cf30 | |
|
|
220e05d2ae | |
|
|
c1cf31f57b | |
|
|
aa319a6bda | |
|
|
026e99511c | |
|
|
21c0c2b95c | |
|
|
1a6d78df43 | |
|
|
b1831ada04 | |
|
|
649bd77bbb | |
|
|
8bfc2ba4f5 | |
|
|
7bf20bda14 | |
|
|
c1f7f27005 | |
|
|
c86337832a | |
|
|
d686c385e0 | |
|
|
0f52c3adc2 | |
|
|
36bc33860f | |
|
|
1b7163ee1a | |
|
|
c0df38c7ba | |
|
|
4e997ae36b | |
|
|
929b68299a | |
|
|
bfc89501ba | |
|
|
d50f705c44 | |
|
|
708a0fbd1f | |
|
|
bbbdd31311 | |
|
|
38ade7562e | |
|
|
385a10e2e7 | |
|
|
2335a413cb | |
|
|
e622013b3d | |
|
|
17d4cc297c | |
|
|
afc66a4971 | |
|
|
c161957cfe | |
|
|
0e0d433ce3 | |
|
|
95c8148787 | |
|
|
52d95b4798 | |
|
|
43ead0e7f2 | |
|
|
935c737fe3 | |
|
|
5888ff9c9e | |
|
|
27be48464a | |
|
|
20167ad359 | |
|
|
0ca031282b | |
|
|
f90bf63354 | |
|
|
95caa2d10c | |
|
|
63d8e17392 | |
|
|
52389292a7 | |
|
|
dd86d5e63c | |
|
|
495594913f | |
|
|
efc4768ed7 | |
|
|
4e81571f2b | |
|
|
46ea3612fd | |
|
|
eb27f01616 | |
|
|
5cff85d260 | |
|
|
863ec614f4 | |
|
|
7a97603106 | |
|
|
0f3ec495a5 | |
|
|
5e605efa26 | |
|
|
55cbd8778a | |
|
|
66c92bb7b1 | |
|
|
abb31a39bb | |
|
|
18cf5e3269 | |
|
|
262221e300 | |
|
|
ed9e36c213 | |
|
|
38dda2f807 | |
|
|
60b1ac1442 | |
|
|
2b175a21f4 | |
|
|
d09daa1503 | |
|
|
3ca511924e | |
|
|
cb4fa2aaba | |
|
|
b8569c6641 | |
|
|
2392dca6fc | |
|
|
8cfd4024e1 | |
|
|
593eee3a34 | |
|
|
0b6c305024 | |
|
|
9a85343166 | |
|
|
89b7627bcd | |
|
|
969b53637a | |
|
|
5ed2d42377 | |
|
|
a6f37fd3dc | |
|
|
72068d003a | |
|
|
3336384434 | |
|
|
1acd9fc3b2 | |
|
|
bb7399df07 | |
|
|
6b4250b903 | |
|
|
076184aad2 | |
|
|
19efe4ada5 | |
|
|
fc96c958ba | |
|
|
4f6d9a689d | |
|
|
4ed2fa4d65 | |
|
|
5afa373b1f | |
|
|
27853a9447 | |
|
|
e8c0828d91 | |
|
|
5ec689101e | |
|
|
4e422fc477 | |
|
|
9ccd94d927 | |
|
|
52b217c180 | |
|
|
f6461ae563 | |
|
|
fc0e913b8a | |
|
|
df8cbb3e80 | |
|
|
f2528fcb39 | |
|
|
ea610a243a | |
|
|
9cc93b88ff | |
|
|
aec516b8dc | |
|
|
350d567f3e | |
|
|
ab385f4bba | |
|
|
bfdf061ead | |
|
|
f2bd7edf7e | |
|
|
9614ce3973 | |
|
|
5af41ad90b | |
|
|
9e9aa01b03 | |
|
|
bfd90792f8 | |
|
|
5eab4669f0 | |
|
|
2289c88320 | |
|
|
a466e523d9 | |
|
|
51e1392640 | |
|
|
2395a8d6b7 | |
|
|
59417b76aa | |
|
|
92bfac8cd7 | |
|
|
0d1a19e852 | |
|
|
0006c04c7d | |
|
|
97165ab007 | |
|
|
f35ba75966 | |
|
|
6842a00890 | |
|
|
95f668d40d | |
|
|
b1ec674fa9 | |
|
|
df04afa5de | |
|
|
d0ebb82f90 | |
|
|
505930b3ec | |
|
|
fb02e5b389 | |
|
|
84426b82cf | |
|
|
5d391f0cee | |
|
|
beb873f9f1 | |
|
|
70cb50e446 | |
|
|
4294e6206b | |
|
|
4473743d5f | |
|
|
14d6406a61 | |
|
|
ae3261d9bc | |
|
|
6b8d437a22 | |
|
|
5c6efa861d | |
|
|
dd561f6e7e | |
|
|
8c08e7f8e9 | |
|
|
56d069f853 | |
|
|
0512a3214c | |
|
|
4e12f93da4 | |
|
|
c551e82eee | |
|
|
1d93b65060 | |
|
|
300542d922 | |
|
|
e065835c4d | |
|
|
2bbb5d7013 | |
|
|
0ef0332e08 | |
|
|
eac2fa63b1 | |
|
|
960b1c9946 | |
|
|
bae50ffda1 | |
|
|
ced25c9a54 | |
|
|
929cfb2b61 | |
|
|
308f05ca07 | |
|
|
225fd50ca1 | |
|
|
9785f098d8 | |
|
|
1116fb350a | |
|
|
6f45efef03 | |
|
|
bd7bf69a99 | |
|
|
7a71fc6ca7 | |
|
|
5b44a41651 | |
|
|
86a73267cb | |
|
|
578cca2687 | |
|
|
403b3da36d | |
|
|
dc523d86c3 | |
|
|
e97fd05e75 | |
|
|
73e3d56381 | |
|
|
8253be0048 | |
|
|
a8432b83ba | |
|
|
8894216ee8 | |
|
|
b05a883353 | |
|
|
219f7724e7 | |
|
|
4f3e9ec19e | |
|
|
3c8c2ebcf4 | |
|
|
9e1a54c738 | |
|
|
45029bf5f4 | |
|
|
f825d65bfc | |
|
|
a017c1352a | |
|
|
30ee36f881 | |
|
|
2024299c02 | |
|
|
c65f436009 | |
|
|
e29eaceeff | |
|
|
1aacd829f2 | |
|
|
f8c0fe9499 | |
|
|
0ea5f3d5e4 | |
|
|
7118a723f3 | |
|
|
d7f900d8ae | |
|
|
b4d216b7c8 | |
|
|
a1c040ddf8 | |
|
|
423ef6231a | |
|
|
2b035ce6e1 | |
|
|
7dc0bbb329 | |
|
|
642f433bcb | |
|
|
946ce1964d | |
|
|
64a775ce53 | |
|
|
78f23ea0a9 | |
|
|
2e500f066f | |
|
|
e653c7c472 | |
|
|
1c71b3aa83 | |
|
|
cd290ad02c | |
|
|
b85f8559e4 | |
|
|
bb4d90fd58 | |
|
|
116da944ad | |
|
|
c552f32370 | |
|
|
79d8f0b160 | |
|
|
84eb035069 | |
|
|
9d368b1864 | |
|
|
2dfc3cc681 | |
|
|
420d9c141c | |
|
|
c2bfc9366c | |
|
|
2e53cc9203 | |
|
|
444c087a11 | |
|
|
fedd75ddf5 | |
|
|
a422d32d23 | |
|
|
08dde416b1 | |
|
|
0ac2d78ad3 | |
|
|
6902297b3c | |
|
|
013ddc6975 | |
|
|
6c303c4b8e | |
|
|
d0590b8540 | |
|
|
4f048b7224 | |
|
|
da3768b7c3 | |
|
|
40219fed08 | |
|
|
f9803b0e6c | |
|
|
0aaf63f36c | |
|
|
cd436251f6 | |
|
|
8c3eca8129 | |
|
|
153ec5b65f | |
|
|
40057c7d3c | |
|
|
04565eb480 | |
|
|
43541a12c9 | |
|
|
e28680a66b | |
|
|
33db3933d8 | |
|
|
1cbaf07cfd | |
|
|
22253fada4 | |
|
|
f2bee41336 | |
|
|
15d2a654bd | |
|
|
4e2209bd5d | |
|
|
a424b3b775 | |
|
|
726f6ac395 | |
|
|
e31bb970a2 | |
|
|
73d05b991c | |
|
|
34202be843 | |
|
|
d28e703cd2 | |
|
|
77fcf1a35a | |
|
|
9ebc8c4219 | |
|
|
9f3437d499 | |
|
|
dd867efd0a | |
|
|
9994a47e54 | |
|
|
ad7c5923a6 | |
|
|
21a663a99b | |
|
|
2fb6dd0c32 | |
|
|
1de67a88b5 | |
|
|
f3a0c92564 | |
|
|
5f23c13490 | |
|
|
32139beebc | |
|
|
7ec5a438d4 | |
|
|
e171f5a503 | |
|
|
80a7a8e455 | |
|
|
760e545444 | |
|
|
63c00174e1 | |
|
|
6572519092 | |
|
|
52fd370460 | |
|
|
88ba2f62d2 | |
|
|
b20662ffd2 | |
|
|
593209e26e | |
|
|
942eb079e8 | |
|
|
cf5e233726 | |
|
|
73b8a6f46d | |
|
|
faf4f566f7 | |
|
|
f7dda7a666 | |
|
|
223f5c0251 | |
|
|
dff27c522f | |
|
|
b2cb7922c2 | |
|
|
d13cd478de | |
|
|
de2163bcef | |
|
|
f821a7bff3 | |
|
|
6350fd6592 | |
|
|
ef9f1b94ff | |
|
|
41e4fb89e8 | |
|
|
8bf3bc3f47 | |
|
|
368d641ae8 | |
|
|
3e19218382 | |
|
|
dd1ddd6418 | |
|
|
f845dadc5d | |
|
|
257174d0c6 | |
|
|
d9b7ef9ad4 | |
|
|
61b67c3619 | |
|
|
7043f26ac8 | |
|
|
8c045acab3 | |
|
|
4e7aa0c3b9 | |
|
|
8c96b2d185 | |
|
|
50ee165c37 | |
|
|
4447911892 | |
|
|
0614609f2b | |
|
|
f959ca98bd | |
|
|
b9d6e5854d | |
|
|
51492a8911 | |
|
|
17e212118c | |
|
|
152558d593 | |
|
|
4daa77f9a1 | |
|
|
4ab2761c82 | |
|
|
852de0fb0e | |
|
|
5b5a0d1a23 | |
|
|
7a9ec8d02c | |
|
|
6cd416fdaa | |
|
|
3803b7dce1 | |
|
|
3fca677f3d | |
|
|
314d80ccf0 | |
|
|
42ad8cddb3 | |
|
|
924c95ab89 | |
|
|
3ab8c9b5a0 | |
|
|
95bef976a5 | |
|
|
12128f278c | |
|
|
e0ee375f01 | |
|
|
2fac9371c8 | |
|
|
4fe512aeda | |
|
|
7a7d06e785 | |
|
|
fabd36f4c4 | |
|
|
192b678bce |
|
|
@ -1,4 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<classpath>
|
|
||||||
<classpathentry kind="output" path="WebContent/WEB-INF/classes"/>
|
|
||||||
</classpath>
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"Framelink Figma MCP": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "figma-developer-mcp", "--figma-api-key=figd_NrYdIWf-CnC23NyH6eMym7sBdfbZTuXyS91tI3VS", "--stdio"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -12,19 +12,20 @@ alwaysApply: false
|
||||||
## 목차
|
## 목차
|
||||||
|
|
||||||
1. [V2 컴포넌트 규칙 (최우선)](#1-v2-컴포넌트-규칙-최우선)
|
1. [V2 컴포넌트 규칙 (최우선)](#1-v2-컴포넌트-규칙-최우선)
|
||||||
2. [표준 Props 인터페이스](#2-표준-props-인터페이스)
|
2. [V2 + Zod 레이아웃 저장/로드 시스템 (핵심)](#2-v2--zod-레이아웃-저장로드-시스템-핵심)
|
||||||
3. [멀티테넌시 (company_code)](#3-멀티테넌시-company_code)
|
3. [표준 Props 인터페이스](#3-표준-props-인터페이스)
|
||||||
4. [디자인 모드 vs 인터랙티브 모드](#4-디자인-모드-vs-인터랙티브-모드)
|
4. [멀티테넌시 (company_code)](#4-멀티테넌시-company_code)
|
||||||
5. [로딩 및 에러 처리](#5-로딩-및-에러-처리)
|
5. [디자인 모드 vs 인터랙티브 모드](#5-디자인-모드-vs-인터랙티브-모드)
|
||||||
6. [테이블 컬럼 기반 입력 위젯](#6-테이블-컬럼-기반-입력-위젯)
|
6. [로딩 및 에러 처리](#6-로딩-및-에러-처리)
|
||||||
7. [컴포넌트별 테이블 설정](#7-컴포넌트별-테이블-설정)
|
7. [테이블 컬럼 기반 입력 위젯](#7-테이블-컬럼-기반-입력-위젯)
|
||||||
8. [엔티티 조인 컬럼 활용](#8-엔티티-조인-컬럼-활용)
|
8. [컴포넌트별 테이블 설정](#8-컴포넌트별-테이블-설정)
|
||||||
9. [폼 데이터 관리](#9-폼-데이터-관리)
|
9. [엔티티 조인 컬럼 활용](#9-엔티티-조인-컬럼-활용)
|
||||||
10. [다국어 지원](#10-다국어-지원)
|
10. [폼 데이터 관리](#10-폼-데이터-관리)
|
||||||
11. [저장 버튼 및 플로우 연동](#11-저장-버튼-및-플로우-연동)
|
11. [다국어 지원](#11-다국어-지원)
|
||||||
12. [표준 코드 스타일 가이드](#12-표준-코드-스타일-가이드)
|
12. [저장 버튼 및 플로우 연동](#12-저장-버튼-및-플로우-연동)
|
||||||
13. [성능 최적화](#13-성능-최적화)
|
13. [표준 코드 스타일 가이드](#13-표준-코드-스타일-가이드)
|
||||||
14. [체크리스트](#14-체크리스트)
|
14. [성능 최적화](#14-성능-최적화)
|
||||||
|
15. [체크리스트](#15-체크리스트)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -50,7 +51,7 @@ alwaysApply: false
|
||||||
| `v2-split-panel-layout` | 분할 패널 | 좌우/상하 분할 |
|
| `v2-split-panel-layout` | 분할 패널 | 좌우/상하 분할 |
|
||||||
| `v2-numbering-rule` | 채번 규칙 | 자동 채번 생성 |
|
| `v2-numbering-rule` | 채번 규칙 | 자동 채번 생성 |
|
||||||
| `v2-tabs-widget` | 탭 위젯 | 탭 레이아웃 |
|
| `v2-tabs-widget` | 탭 위젯 | 탭 레이아웃 |
|
||||||
| `v2-unified-repeater` | 통합 리피터 | 행 단위 입력/저장 |
|
| `v2-repeater` | 통합 리피터 | 행 단위 입력/저장 |
|
||||||
| `v2-rack-structure` | 렉 구조 | 창고 렉 위치 생성 |
|
| `v2-rack-structure` | 렉 구조 | 창고 렉 위치 생성 |
|
||||||
| `v2-section-paper` | 섹션 페이퍼 | 섹션 컨테이너 |
|
| `v2-section-paper` | 섹션 페이퍼 | 섹션 컨테이너 |
|
||||||
| `v2-section-card` | 섹션 카드 | 카드 컨테이너 |
|
| `v2-section-card` | 섹션 카드 | 카드 컨테이너 |
|
||||||
|
|
@ -90,7 +91,212 @@ export const V2TableListDefinition = createComponentDefinition({
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 표준 Props 인터페이스
|
## 2. V2 + Zod 레이아웃 저장/로드 시스템 (핵심)
|
||||||
|
|
||||||
|
### 핵심 원칙
|
||||||
|
|
||||||
|
**컴포넌트 코드 수정 시 모든 화면에 자동 반영되도록 V2 + Zod 기반 저장/로드 방식을 사용합니다.**
|
||||||
|
|
||||||
|
```
|
||||||
|
저장: component_url + overrides (차이값만)
|
||||||
|
로드: Zod 기본값 + overrides 병합
|
||||||
|
```
|
||||||
|
|
||||||
|
### 기존 방식 vs V2 방식
|
||||||
|
|
||||||
|
| 항목 | 기존 (문제점) | V2 (해결) |
|
||||||
|
|-----|-------------|----------|
|
||||||
|
| 저장 | 전체 설정 "박제" | url + overrides (차이값만) |
|
||||||
|
| 코드 수정 반영 | 안 됨 | 자동 반영 |
|
||||||
|
| 테이블 | `screen_layouts` (다중 레코드) | `screen_layouts_v2` (1 레코드) |
|
||||||
|
|
||||||
|
### 저장 구조 (screen_layouts_v2)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "2.0",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"id": "comp_xxx",
|
||||||
|
"url": "@/lib/registry/components/v2-select",
|
||||||
|
"position": { "x": 100, "y": 50 },
|
||||||
|
"size": { "width": 180, "height": 30 },
|
||||||
|
"displayOrder": 0,
|
||||||
|
"overrides": {
|
||||||
|
"tableName": "warehouse_info",
|
||||||
|
"columnName": "warehouse_code",
|
||||||
|
"label": "창고코드",
|
||||||
|
"webType": "select"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 핵심 필드: overrides에 반드시 포함되어야 하는 속성
|
||||||
|
|
||||||
|
컴포넌트가 테이블 컬럼에서 드래그되어 생성된 경우, 다음 속성들이 `overrides`에 저장됩니다:
|
||||||
|
|
||||||
|
| 속성 | 설명 | 예시 |
|
||||||
|
|-----|------|-----|
|
||||||
|
| `tableName` | 연결된 테이블명 | `"warehouse_info"` |
|
||||||
|
| `columnName` | 연결된 컬럼명 | `"warehouse_code"` |
|
||||||
|
| `label` | 표시 라벨 | `"창고코드"` |
|
||||||
|
| `required` | 필수 입력 여부 | `true` / `false` |
|
||||||
|
| `readonly` | 읽기 전용 여부 | `true` / `false` |
|
||||||
|
| `inputType` | 입력 타입 | `"text"`, `"select"`, `"date"` 등 |
|
||||||
|
| `webType` | 웹 타입 | `"text"`, `"select"`, `"date"` 등 |
|
||||||
|
| `codeCategory` | 코드 카테고리 (코드 타입인 경우) | `"WAREHOUSE_TYPE"` |
|
||||||
|
|
||||||
|
### 저장 로직 (convertLegacyToV2)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// frontend/lib/utils/layoutV2Converter.ts
|
||||||
|
|
||||||
|
export function convertLegacyToV2(legacyLayout: LegacyLayoutData): LayoutV2 {
|
||||||
|
const components = legacyLayout.components.map((comp, index) => {
|
||||||
|
const componentType = comp.componentType || comp.widgetType || comp.type;
|
||||||
|
const url = getComponentUrl(componentType);
|
||||||
|
const defaults = getDefaultsByUrl(url);
|
||||||
|
|
||||||
|
// 상위 레벨 속성들도 overrides에 포함 (중요!)
|
||||||
|
const topLevelProps: Record<string, any> = {};
|
||||||
|
if (comp.tableName) topLevelProps.tableName = comp.tableName;
|
||||||
|
if (comp.columnName) topLevelProps.columnName = comp.columnName;
|
||||||
|
if (comp.label) topLevelProps.label = comp.label;
|
||||||
|
if (comp.required !== undefined) topLevelProps.required = comp.required;
|
||||||
|
if (comp.readonly !== undefined) topLevelProps.readonly = comp.readonly;
|
||||||
|
|
||||||
|
// componentConfig에서 차이값만 추출
|
||||||
|
const configOverrides = extractCustomConfig(comp.componentConfig || {}, defaults);
|
||||||
|
|
||||||
|
// 병합
|
||||||
|
const overrides = { ...topLevelProps, ...configOverrides };
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: comp.id,
|
||||||
|
url: url,
|
||||||
|
position: comp.position,
|
||||||
|
size: comp.size,
|
||||||
|
displayOrder: index,
|
||||||
|
overrides: overrides,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { version: "2.0", components };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 로드 로직 (convertV2ToLegacy)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// frontend/lib/utils/layoutV2Converter.ts
|
||||||
|
|
||||||
|
export function convertV2ToLegacy(v2Layout: LayoutV2): LegacyLayoutData {
|
||||||
|
const components = v2Layout.components.map((comp) => {
|
||||||
|
const componentType = getComponentTypeFromUrl(comp.url);
|
||||||
|
const defaults = getDefaultsByUrl(comp.url);
|
||||||
|
const mergedConfig = mergeComponentConfig(defaults, comp.overrides);
|
||||||
|
|
||||||
|
// overrides에서 상위 레벨 속성들 복원
|
||||||
|
const overrides = comp.overrides || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: comp.id,
|
||||||
|
componentType: componentType,
|
||||||
|
position: comp.position,
|
||||||
|
size: comp.size,
|
||||||
|
componentConfig: mergedConfig,
|
||||||
|
// 상위 레벨 속성 복원 (중요!)
|
||||||
|
tableName: overrides.tableName,
|
||||||
|
columnName: overrides.columnName,
|
||||||
|
label: overrides.label || "",
|
||||||
|
required: overrides.required,
|
||||||
|
readonly: overrides.readonly,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { components };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zod 스키마 구조
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// frontend/lib/schemas/componentConfig.ts
|
||||||
|
|
||||||
|
// 컴포넌트별 overrides 스키마
|
||||||
|
export const v2SelectOverridesSchema = z.object({
|
||||||
|
mode: z.enum(["dropdown", "combobox", "radio", "checkbox"]).default("dropdown"),
|
||||||
|
source: z.enum(["static", "code", "entity", "db", "distinct"]).default("distinct"),
|
||||||
|
multiple: z.boolean().default(false),
|
||||||
|
searchable: z.boolean().default(true),
|
||||||
|
placeholder: z.string().default("선택하세요"),
|
||||||
|
}).passthrough(); // 정의되지 않은 필드도 통과 (tableName, columnName 등)
|
||||||
|
|
||||||
|
// 스키마 레지스트리
|
||||||
|
export const componentOverridesSchemaRegistry: Record<string, z.ZodType<any>> = {
|
||||||
|
"v2-select": v2SelectOverridesSchema,
|
||||||
|
"v2-input": v2InputOverridesSchema,
|
||||||
|
"v2-table-list": v2TableListOverridesSchema,
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
|
||||||
|
// 기본값 레지스트리
|
||||||
|
export const componentDefaultsRegistry: Record<string, any> = {
|
||||||
|
"v2-select": {
|
||||||
|
mode: "dropdown",
|
||||||
|
source: "distinct", // 기본: 테이블 컬럼에서 자동 로드
|
||||||
|
multiple: false,
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### v2-select 자동 옵션 로드
|
||||||
|
|
||||||
|
`webType`이 `"select"`인 컬럼을 드래그하면:
|
||||||
|
|
||||||
|
1. **저장 시**: `tableName`, `columnName`이 `overrides`에 저장됨
|
||||||
|
2. **로드 시**: `source`가 `"distinct"`이면 자동으로 `/entity/{tableName}/distinct/{columnName}` API 호출
|
||||||
|
3. **결과**: 해당 컬럼의 고유 값들이 옵션으로 표시됨
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// DynamicComponentRenderer.tsx
|
||||||
|
|
||||||
|
case "v2-select":
|
||||||
|
return (
|
||||||
|
<V2Select
|
||||||
|
{...commonProps}
|
||||||
|
config={{
|
||||||
|
mode: config.mode || "dropdown",
|
||||||
|
source: config.source || "distinct", // 기본: 테이블에서 자동 로드
|
||||||
|
// ...
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 관련 파일
|
||||||
|
|
||||||
|
| 파일 | 역할 |
|
||||||
|
|------|------|
|
||||||
|
| `frontend/lib/schemas/componentConfig.ts` | Zod 스키마 및 기본값 레지스트리 |
|
||||||
|
| `frontend/lib/utils/layoutV2Converter.ts` | V2 ↔ Legacy 변환 유틸리티 |
|
||||||
|
| `frontend/lib/api/screen.ts` | `getLayoutV2`, `saveLayoutV2` API |
|
||||||
|
| `backend-node/src/services/screenManagementService.ts` | 백엔드 저장/로드 로직 |
|
||||||
|
|
||||||
|
### 새 컴포넌트 추가 시 체크리스트
|
||||||
|
|
||||||
|
1. [ ] `componentConfig.ts`에 Zod 스키마 추가 (`.passthrough()` 필수)
|
||||||
|
2. [ ] `componentOverridesSchemaRegistry`에 등록
|
||||||
|
3. [ ] `componentDefaultsRegistry`에 기본값 등록
|
||||||
|
4. [ ] 테이블 컬럼 드래그 시 `tableName`, `columnName` 저장 확인
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 표준 Props 인터페이스
|
||||||
|
|
||||||
### 컴포넌트가 받아야 하는 표준 Props
|
### 컴포넌트가 받아야 하는 표준 Props
|
||||||
|
|
||||||
|
|
@ -150,7 +356,7 @@ export const MyComponent: React.FC<StandardComponentProps> = ({
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 멀티테넌시 (company_code)
|
## 4. 멀티테넌시 (company_code)
|
||||||
|
|
||||||
### 핵심 원칙
|
### 핵심 원칙
|
||||||
|
|
||||||
|
|
@ -199,7 +405,7 @@ const response = await apiClient.post(`/table-management/tables/${tableName}/add
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 디자인 모드 vs 인터랙티브 모드
|
## 5. 디자인 모드 vs 인터랙티브 모드
|
||||||
|
|
||||||
### 모드 구분
|
### 모드 구분
|
||||||
|
|
||||||
|
|
@ -257,7 +463,7 @@ const handleClick = useCallback(() => {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 로딩 및 에러 처리
|
## 6. 로딩 및 에러 처리
|
||||||
|
|
||||||
### 로딩 상태 관리
|
### 로딩 상태 관리
|
||||||
|
|
||||||
|
|
@ -378,7 +584,7 @@ if (!silentActions.includes(actionType)) {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. 테이블 컬럼 기반 입력 위젯
|
## 7. 테이블 컬럼 기반 입력 위젯
|
||||||
|
|
||||||
### 드래그 방식으로 입력 폼 생성
|
### 드래그 방식으로 입력 폼 생성
|
||||||
|
|
||||||
|
|
@ -398,26 +604,26 @@ if (!silentActions.includes(actionType)) {
|
||||||
|
|
||||||
| inputType | 생성 위젯 | 설명 |
|
| inputType | 생성 위젯 | 설명 |
|
||||||
|-----------|----------|------|
|
|-----------|----------|------|
|
||||||
| `text`, `textarea` | UnifiedInput | 텍스트 입력 |
|
| `text`, `textarea` | V2Input | 텍스트 입력 |
|
||||||
| `number` | UnifiedInput | 숫자 입력 |
|
| `number` | V2Input | 숫자 입력 |
|
||||||
| `date`, `datetime` | UnifiedDate | 날짜/시간 선택 |
|
| `date`, `datetime` | V2Date | 날짜/시간 선택 |
|
||||||
| `code`, `category`, `entity` | UnifiedSelect | 선택박스 |
|
| `code`, `category`, `entity` | V2Select | 선택박스 |
|
||||||
| `checkbox`, `radio` | 체크박스/라디오 | 선택 |
|
| `checkbox`, `radio` | 체크박스/라디오 | 선택 |
|
||||||
| `image`, `file` | UnifiedMedia | 파일 업로드 |
|
| `image`, `file` | V2Media | 파일 업로드 |
|
||||||
|
|
||||||
### 핵심 Unified 컴포넌트 (3개)
|
### 핵심 V2 컴포넌트 (3개)
|
||||||
|
|
||||||
| 컴포넌트 | 담당 inputType | 파일 경로 |
|
| 컴포넌트 | 담당 inputType | 파일 경로 |
|
||||||
|----------|---------------|-----------|
|
|----------|---------------|-----------|
|
||||||
| `UnifiedInput` | text, textarea, number, password | `components/unified/UnifiedInput.tsx` |
|
| `V2Input` | text, textarea, number, password | `components/v2/V2Input.tsx` |
|
||||||
| `UnifiedSelect` | code, category, entity, select | `components/unified/UnifiedSelect.tsx` |
|
| `V2Select` | code, category, entity, select | `components/v2/V2Select.tsx` |
|
||||||
| `UnifiedDate` | date, datetime, time, daterange | `components/unified/UnifiedDate.tsx` |
|
| `V2Date` | date, datetime, time, daterange | `components/v2/V2Date.tsx` |
|
||||||
|
|
||||||
### 컴포넌트 패널에서 직접 드래그 가능한 컴포넌트
|
### 컴포넌트 패널에서 직접 드래그 가능한 컴포넌트
|
||||||
|
|
||||||
| 컴포넌트 ID | 이름 | 설명 |
|
| 컴포넌트 ID | 이름 | 설명 |
|
||||||
|-------------|------|------|
|
|-------------|------|------|
|
||||||
| `v2-unified-repeater` | 리피터 그리드 | 행 단위 데이터 추가/수정/삭제 |
|
| `v2-repeater` | 리피터 그리드 | 행 단위 데이터 추가/수정/삭제 |
|
||||||
| `v2-table-list` | 테이블 리스트 | 데이터 목록 조회/필터/정렬 |
|
| `v2-table-list` | 테이블 리스트 | 데이터 목록 조회/필터/정렬 |
|
||||||
| `v2-table-search-widget` | 검색 필터 | 테이블 검색 조건 입력 |
|
| `v2-table-search-widget` | 검색 필터 | 테이블 검색 조건 입력 |
|
||||||
| `v2-button-primary` | 버튼 | 저장, 삭제, 조회 등 액션 |
|
| `v2-button-primary` | 버튼 | 저장, 삭제, 조회 등 액션 |
|
||||||
|
|
@ -492,7 +698,7 @@ const handleSave = async (context: ButtonActionContext) => {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. 컴포넌트별 테이블 설정
|
## 8. 컴포넌트별 테이블 설정
|
||||||
|
|
||||||
### 핵심 원칙
|
### 핵심 원칙
|
||||||
|
|
||||||
|
|
@ -519,7 +725,7 @@ interface TableListConfig {
|
||||||
#### 저장용 (리피터)
|
#### 저장용 (리피터)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface UnifiedRepeaterConfig {
|
interface V2RepeaterConfig {
|
||||||
mainTableName?: string; // 저장할 테이블명
|
mainTableName?: string; // 저장할 테이블명
|
||||||
useCustomTable?: boolean; // true: mainTableName 사용
|
useCustomTable?: boolean; // true: mainTableName 사용
|
||||||
foreignKeyColumn?: string; // FK 컬럼 (예: receiving_id)
|
foreignKeyColumn?: string; // FK 컬럼 (예: receiving_id)
|
||||||
|
|
@ -606,7 +812,7 @@ const response = await apiClient.get(
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 엔티티 조인 컬럼 활용
|
## 9. 엔티티 조인 컬럼 활용
|
||||||
|
|
||||||
### 핵심 원칙
|
### 핵심 원칙
|
||||||
|
|
||||||
|
|
@ -712,9 +918,9 @@ const getEntityJoinValue = (item: any, columnName: string): any => {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. 폼 데이터 관리
|
## 10. 폼 데이터 관리
|
||||||
|
|
||||||
### 통합 폼 시스템 (UnifiedFormContext)
|
### 통합 폼 시스템 (V2FormContext)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { useFormCompatibility } from "@/hooks/useFormCompatibility";
|
import { useFormCompatibility } from "@/hooks/useFormCompatibility";
|
||||||
|
|
@ -743,20 +949,20 @@ const MyComponent = ({ onFormDataChange, formData }) => {
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const handleChange = useCallback((value: any) => {
|
const handleChange = useCallback((value: any) => {
|
||||||
// 1. UnifiedFormContext
|
// 1. V2FormContext
|
||||||
unifiedContext?.setValue(fieldName, value);
|
v2Context?.setValue(fieldName, value);
|
||||||
|
|
||||||
// 2. ScreenContext
|
// 2. ScreenContext
|
||||||
screenContext?.updateFormData?.(fieldName, value);
|
screenContext?.updateFormData?.(fieldName, value);
|
||||||
|
|
||||||
// 3. 레거시 콜백
|
// 3. 레거시 콜백
|
||||||
onFormDataChange?.(fieldName, value);
|
onFormDataChange?.(fieldName, value);
|
||||||
}, [fieldName, unifiedContext, screenContext, onFormDataChange]);
|
}, [fieldName, v2Context, screenContext, onFormDataChange]);
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. 다국어 지원
|
## 11. 다국어 지원
|
||||||
|
|
||||||
### 타입 정의 시 다국어 필드 추가
|
### 타입 정의 시 다국어 필드 추가
|
||||||
|
|
||||||
|
|
@ -824,7 +1030,7 @@ const MyComponent = ({ component }) => {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. 저장 버튼 및 플로우 연동
|
## 12. 저장 버튼 및 플로우 연동
|
||||||
|
|
||||||
### beforeFormSave 이벤트 처리 (필수)
|
### beforeFormSave 이벤트 처리 (필수)
|
||||||
|
|
||||||
|
|
@ -912,9 +1118,9 @@ const MyFormComponent = ({ formData, onFormDataChange }) => {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. 표준 코드 스타일 가이드
|
## 13. 표준 코드 스타일 가이드
|
||||||
|
|
||||||
**`v2-unified-repeater`** 컴포넌트를 표준으로 삼아 동일한 구조로 작성합니다.
|
**`v2-repeater`** 컴포넌트를 표준으로 삼아 동일한 구조로 작성합니다.
|
||||||
|
|
||||||
### 핵심 원칙: 느슨한 결합도 (Loose Coupling)
|
### 핵심 원칙: 느슨한 결합도 (Loose Coupling)
|
||||||
|
|
||||||
|
|
@ -1276,7 +1482,7 @@ const tableName = config?.dataSource?.tableName;
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 13. 성능 최적화
|
## 14. 성능 최적화
|
||||||
|
|
||||||
### useMemo로 계산 비용 줄이기
|
### useMemo로 계산 비용 줄이기
|
||||||
|
|
||||||
|
|
@ -1452,7 +1658,7 @@ const derivedValue = useMemo(() => data.map(x => x.value), [data]);
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14. 체크리스트
|
## 15. 체크리스트
|
||||||
|
|
||||||
### V2 컴포넌트 규칙
|
### V2 컴포넌트 규칙
|
||||||
|
|
||||||
|
|
@ -1461,6 +1667,16 @@ const derivedValue = useMemo(() => data.map(x => x.value), [data]);
|
||||||
- [ ] Definition 이름에 `V2` 접두사 사용
|
- [ ] Definition 이름에 `V2` 접두사 사용
|
||||||
- [ ] 원본 폴더 수정하지 않음
|
- [ ] 원본 폴더 수정하지 않음
|
||||||
|
|
||||||
|
### V2 + Zod 레이아웃 시스템
|
||||||
|
|
||||||
|
- [ ] `componentConfig.ts`에 Zod 스키마 추가 (`.passthrough()` 필수)
|
||||||
|
- [ ] `componentOverridesSchemaRegistry`에 컴포넌트 등록
|
||||||
|
- [ ] `componentDefaultsRegistry`에 기본값 등록
|
||||||
|
- [ ] 테이블 컬럼 드래그 시 `tableName`, `columnName` 저장 확인
|
||||||
|
- [ ] `convertLegacyToV2`에서 상위 레벨 속성 포함 확인
|
||||||
|
- [ ] `convertV2ToLegacy`에서 상위 레벨 속성 복원 확인
|
||||||
|
- [ ] v2-select는 `source: "distinct"` 기본값 확인
|
||||||
|
|
||||||
### 표준 Props
|
### 표준 Props
|
||||||
|
|
||||||
- [ ] `component`, `isDesignMode` props 지원
|
- [ ] `component`, `isDesignMode` props 지원
|
||||||
|
|
@ -1533,7 +1749,7 @@ const derivedValue = useMemo(() => data.map(x => x.value), [data]);
|
||||||
|
|
||||||
### 코드 스타일
|
### 코드 스타일
|
||||||
|
|
||||||
- [ ] `v2-unified-repeater` 구조 참고
|
- [ ] `v2-repeater` 구조 참고
|
||||||
- [ ] 느슨한 결합도 유지 (이벤트 기반 통신)
|
- [ ] 느슨한 결합도 유지 (이벤트 기반 통신)
|
||||||
|
|
||||||
### 성능 최적화
|
### 성능 최적화
|
||||||
|
|
@ -1553,9 +1769,9 @@ const derivedValue = useMemo(() => data.map(x => x.value), [data]);
|
||||||
|
|
||||||
| 파일 | 역할 |
|
| 파일 | 역할 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `components/unified/UnifiedInput.tsx` | text, number 입력 |
|
| `components/v2/V2Input.tsx` | text, number 입력 |
|
||||||
| `components/unified/UnifiedSelect.tsx` | code, entity 선택 |
|
| `components/v2/V2Select.tsx` | code, entity 선택 |
|
||||||
| `components/unified/UnifiedDate.tsx` | date, datetime 선택 |
|
| `components/v2/V2Date.tsx` | date, datetime 선택 |
|
||||||
| `lib/registry/components/v2-*/` | V2 컴포넌트 폴더 |
|
| `lib/registry/components/v2-*/` | V2 컴포넌트 폴더 |
|
||||||
| `lib/api/entityJoin.ts` | 엔티티 조인 API |
|
| `lib/api/entityJoin.ts` | 엔티티 조인 API |
|
||||||
| `hooks/useFormCompatibility.ts` | 폼 호환성 브릿지 |
|
| `hooks/useFormCompatibility.ts` | 폼 호환성 브릿지 |
|
||||||
|
|
@ -1567,6 +1783,6 @@ const derivedValue = useMemo(() => data.map(x => x.value), [data]);
|
||||||
|
|
||||||
| 컴포넌트 | 경로 | 참고 사항 |
|
| 컴포넌트 | 경로 | 참고 사항 |
|
||||||
|----------|------|-----------|
|
|----------|------|-----------|
|
||||||
| `v2-unified-repeater` | `lib/registry/components/v2-unified-repeater/` | **표준 참조 컴포넌트** |
|
| `v2-repeater` | `lib/registry/components/v2-repeater/` | **표준 참조 컴포넌트** |
|
||||||
| `v2-table-list` | `lib/registry/components/v2-table-list/` | 조회 컴포넌트 참조 |
|
| `v2-table-list` | `lib/registry/components/v2-table-list/` | 조회 컴포넌트 참조 |
|
||||||
| `v2-table-search-widget` | `lib/registry/components/v2-table-search-widget/` | 검색 필터 참조 |
|
| `v2-table-search-widget` | `lib/registry/components/v2-table-search-widget/` | 검색 필터 참조 |
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
```
|
|
||||||
|
|
|
||||||
|
|
@ -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 배포
|
|
||||||
|
|
@ -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 응답 속도 최적화
|
|
||||||
- 클라이언트 사이드 캐싱
|
|
||||||
- 이미지 및 정적 자원 최적화
|
|
||||||
|
|
@ -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. 트랜잭션 처리는 서비스 레벨에서 관리
|
|
||||||
|
|
@ -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 파이프라인
|
||||||
|
|
|
||||||
|
|
@ -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 로깅 모니터링
|
||||||
- [ ] 로그 모니터링
|
|
||||||
- [ ] 권한 정기 검토
|
- [ ] 권한 정기 검토
|
||||||
- [ ] 패스워드 정책 적용
|
- [ ] 패스워드 정책 적용
|
||||||
- [ ] 백업 데이터 암호화
|
- [ ] 백업 데이터 암호화
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ CREATE TABLE "테이블명" (
|
||||||
-- 시스템 기본 컬럼 (자동 포함)
|
-- 시스템 기본 컬럼 (자동 포함)
|
||||||
"id" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
"id" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
||||||
"created_date" timestamp DEFAULT now(),
|
"created_date" timestamp DEFAULT now(),
|
||||||
"updated_date" timestamp DEFAULT now(),
|
"updated_date" timestamp DEFAULT now(),b
|
||||||
"writer" varchar(500) DEFAULT NULL,
|
"writer" varchar(500) DEFAULT NULL,
|
||||||
"company_code" varchar(500),
|
"company_code" varchar(500),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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. 사용자 입력값 검증
|
|
||||||
```
|
|
||||||
43
.cursorrules
|
|
@ -1,5 +1,48 @@
|
||||||
# Cursor Rules for ERP-node Project
|
# Cursor Rules for ERP-node Project
|
||||||
|
|
||||||
|
## 🚨 비즈니스 로직 요청 양식 검증 (필수)
|
||||||
|
|
||||||
|
**사용자가 화면 개발 또는 비즈니스 로직 구현을 요청할 때, 아래 양식을 따르지 않으면 반드시 다음과 같이 응답하세요:**
|
||||||
|
|
||||||
|
```
|
||||||
|
안녕하세요. Oh My Master! 양식을 못 알아 듣겠습니다.
|
||||||
|
다시 한번 작성해주십쇼.
|
||||||
|
=== 비즈니스 로직 요청서 ===
|
||||||
|
|
||||||
|
【화면 정보】
|
||||||
|
- 화면명:
|
||||||
|
- 회사코드:
|
||||||
|
- 메뉴ID (있으면):
|
||||||
|
|
||||||
|
【테이블 정보】
|
||||||
|
- 메인 테이블:
|
||||||
|
- 디테일 테이블 (있으면):
|
||||||
|
- 관계 FK (있으면):
|
||||||
|
|
||||||
|
【버튼 목록】
|
||||||
|
버튼1:
|
||||||
|
- 버튼명:
|
||||||
|
- 동작 유형: (저장/삭제/수정/조회/기타)
|
||||||
|
- 조건 (있으면):
|
||||||
|
- 대상 테이블:
|
||||||
|
- 추가 동작 (있으면):
|
||||||
|
|
||||||
|
【추가 요구사항】
|
||||||
|
-
|
||||||
|
```
|
||||||
|
|
||||||
|
**양식 미준수 판단 기준:**
|
||||||
|
1. "화면 만들어줘" 같이 테이블명/버튼 정보 없이 요청
|
||||||
|
2. "저장하면 저장해줘" 같이 구체적인 테이블/로직 설명 없음
|
||||||
|
3. "이전이랑 비슷하게" 같이 모호한 참조
|
||||||
|
4. 버튼별 조건/동작이 명시되지 않음
|
||||||
|
|
||||||
|
**양식 미준수 시 절대 작업 진행하지 말고, 위 양식을 보여주며 다시 작성하라고 요청하세요.**
|
||||||
|
|
||||||
|
**상세 가이드**: [화면개발_표준_가이드.md](docs/screen-implementation-guide/화면개발_표준_가이드.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🚨 최우선 보안 규칙: 멀티테넌시
|
## 🚨 최우선 보안 규칙: 멀티테넌시
|
||||||
|
|
||||||
**모든 코드 작성/수정 완료 후 반드시 다음 파일을 확인하세요:**
|
**모든 코드 작성/수정 완료 후 반드시 다음 파일을 확인하세요:**
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
47
.project
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
eclipse.preferences.version=1
|
|
||||||
encoding//WebContent/WEB-INF/view/materMgmt/materOrderDown1.jsp=UTF-8
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
org.eclipse.wst.jsdt.launching.baseBrowserLibrary
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Window
|
|
||||||
|
|
@ -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
|
|
||||||
419
PLAN.MD
|
|
@ -1,104 +1,337 @@
|
||||||
# 프로젝트: 화면 관리 기능 개선 (복제/삭제/그룹 관리/테이블 설정)
|
# 현재 구현 계획: pop-card-list 입력 필드/계산 필드 구조 개편
|
||||||
|
|
||||||
## 개요
|
> **작성일**: 2026-02-24
|
||||||
화면 관리 시스템의 복제, 삭제, 수정, 테이블 설정 기능을 전면 개선하여 효율적인 화면 관리를 지원합니다.
|
> **상태**: 계획 완료, 코딩 대기
|
||||||
|
> **목적**: 입력 필드 설정 단순화 + 본문 필드에 계산식 통합 + 기존 계산 필드 섹션 제거
|
||||||
## 핵심 기능
|
|
||||||
|
|
||||||
### 1. 단일 화면 복제
|
|
||||||
- [x] 우클릭 컨텍스트 메뉴에서 "복제" 선택
|
|
||||||
- [x] 화면명, 화면 코드 자동 생성 (중복 시 `_COPY` 접미사 추가)
|
|
||||||
- [x] 연결된 모달 화면 함께 복제
|
|
||||||
- [x] 대상 그룹 선택 가능
|
|
||||||
- [x] 복제 후 목록 자동 새로고침
|
|
||||||
|
|
||||||
### 2. 그룹(폴더) 전체 복제
|
|
||||||
- [x] 대분류 폴더 복제 시 모든 하위 폴더 + 화면 재귀적 복제
|
|
||||||
- [x] 정렬 순서(display_order) 유지
|
|
||||||
- [x] 대분류(최상위 그룹) 복제 시 경고 문구 표시
|
|
||||||
- [x] 정렬 순서 입력 필드 추가
|
|
||||||
- [x] 복제 모드 선택: 전체(폴더+화면), 폴더만, 화면만
|
|
||||||
- [x] 모달 스크롤 지원 (max-h-[90vh] overflow-y-auto)
|
|
||||||
|
|
||||||
### 3. 고급 옵션: 이름 일괄 변경
|
|
||||||
- [x] 찾을 텍스트 / 대체할 텍스트 (Find & Replace)
|
|
||||||
- [x] 미리보기 기능
|
|
||||||
|
|
||||||
### 4. 삭제 기능
|
|
||||||
- [x] 단일 화면 삭제 (휴지통으로 이동)
|
|
||||||
- [x] 그룹 삭제 (화면 함께 삭제 옵션)
|
|
||||||
- [x] 삭제 시 로딩 프로그레스 바 표시
|
|
||||||
|
|
||||||
### 5. 화면 수정 기능
|
|
||||||
- [x] 우클릭 "수정" 메뉴로 화면 이름/그룹/역할/정렬 순서 변경
|
|
||||||
- [x] 그룹 추가/수정 시 상위 그룹 기반 자동 회사 코드 설정
|
|
||||||
|
|
||||||
### 6. 테이블 설정 기능 (TableSettingModal)
|
|
||||||
- [x] 화면 설정 모달에 "테이블 설정" 탭 추가
|
|
||||||
- [x] 입력 타입 변경 시 관련 참조 필드 자동 초기화
|
|
||||||
- 엔티티→텍스트: referenceTable, referenceColumn, displayColumn 초기화
|
|
||||||
- 코드→다른 타입: codeCategory, codeValue 초기화
|
|
||||||
- [x] 데이터 일관성 유지 (inputType ↔ referenceTable 연동)
|
|
||||||
- [x] 조인 배지 단일화 (FK 배지 제거, 조인 배지만 표시)
|
|
||||||
|
|
||||||
### 7. 회사 코드 지원 (최고 관리자)
|
|
||||||
- [x] 대상 회사 선택 가능
|
|
||||||
- [x] 상위 그룹 선택 시 자동 회사 코드 설정
|
|
||||||
|
|
||||||
## 관련 파일
|
|
||||||
- `frontend/components/screen/CopyScreenModal.tsx` - 복제 모달
|
|
||||||
- `frontend/components/screen/ScreenGroupTreeView.tsx` - 트리 뷰 + 컨텍스트 메뉴
|
|
||||||
- `frontend/components/screen/TableSettingModal.tsx` - 테이블 설정 모달
|
|
||||||
- `frontend/components/screen/ScreenSettingModal.tsx` - 화면 설정 모달 (테이블 설정 탭 포함)
|
|
||||||
- `frontend/lib/api/screen.ts` - 화면 API
|
|
||||||
- `frontend/lib/api/screenGroup.ts` - 그룹 API
|
|
||||||
- `frontend/lib/api/tableManagement.ts` - 테이블 관리 API
|
|
||||||
|
|
||||||
## 진행 상태
|
|
||||||
- [완료] 단일 화면 복제 + 새로고침
|
|
||||||
- [완료] 그룹 전체 복제 (재귀적)
|
|
||||||
- [완료] 고급 옵션: 이름 일괄 변경 (Find & Replace)
|
|
||||||
- [완료] 단일 화면/그룹 삭제 + 로딩 프로그레스
|
|
||||||
- [완료] 화면 수정 (이름/그룹/역할/순서)
|
|
||||||
- [완료] 테이블 설정 탭 추가
|
|
||||||
- [완료] 입력 타입 변경 시 관련 필드 초기화
|
|
||||||
- [완료] 그룹 복제 모달 스크롤 문제 수정
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 이전 프로젝트: 외부 REST API 커넥션 관리 확장 (POST/Body 지원)
|
## 1. 변경 개요
|
||||||
|
|
||||||
## 개요
|
### 배경
|
||||||
현재 GET 방식 위주로 구현된 외부 REST API 커넥션 관리 기능을 확장하여, POST, PUT, DELETE 등 다양한 HTTP 메서드와 JSON Request Body를 설정하고 테스트할 수 있도록 개선합니다. 이를 통해 토큰 발급 API나 데이터 전송 API 등 다양한 외부 시스템과의 연동을 지원합니다.
|
- 기존: "입력 필드", "계산 필드", "담기 버튼" 3개가 별도 섹션으로 분리
|
||||||
|
- 문제: 계산 필드가 본문 필드와 동일한 위치에 표시되어야 하는데 별도 영역에 있음
|
||||||
|
- 문제: 입력 필드의 min/max 고정값은 비실용적 (실제로는 DB 컬럼 기준 제한이 필요)
|
||||||
|
- 문제: step, columnName, sourceColumns, resultColumn 등 죽은 코드 존재
|
||||||
|
|
||||||
## 핵심 기능
|
### 목표
|
||||||
1. **DB 스키마 확장**: `external_rest_api_connections` 테이블에 `default_method`, `default_body` 컬럼 추가
|
1. **본문 필드에 계산식 지원 추가** - 필드별로 "DB 컬럼" 또는 "계산식" 선택
|
||||||
2. **백엔드 로직 개선**:
|
2. **입력 필드 설정 단순화** - 고정 min/max 제거, 제한 기준 컬럼 방식으로 변경
|
||||||
- 커넥션 생성/수정 시 메서드와 바디 정보 저장
|
3. **기존 "계산 필드" 섹션 제거** - 본문 필드에 통합되므로 불필요
|
||||||
- 연결 테스트 시 설정된 메서드와 바디를 사용하여 요청 수행
|
4. **죽은 코드 정리**
|
||||||
- SSL 인증서 검증 우회 옵션 적용 (내부망/테스트망 지원)
|
|
||||||
3. **프론트엔드 UI 개선**:
|
|
||||||
- 커넥션 설정 모달에 HTTP 메서드 선택(Select) 및 Body 입력(Textarea/JSON Editor) 필드 추가
|
|
||||||
- 테스트 기능에서 Body 데이터 포함하여 요청 전송
|
|
||||||
|
|
||||||
## 테스트 계획
|
---
|
||||||
### 1단계: 기본 기능 및 DB 마이그레이션
|
|
||||||
- [x] DB 마이그레이션 스크립트 작성 및 실행
|
|
||||||
- [x] 백엔드 타입 정의 수정 (`default_method`, `default_body` 추가)
|
|
||||||
|
|
||||||
### 2단계: 백엔드 로직 구현
|
## 2. 수정 대상 파일 (3개)
|
||||||
- [x] 커넥션 생성/수정 API 수정 (필드 추가)
|
|
||||||
- [x] 커넥션 상세 조회 API 확인
|
|
||||||
- [x] 연결 테스트 API 수정 (Method, Body 반영하여 요청 전송)
|
|
||||||
|
|
||||||
### 3단계: 프론트엔드 구현
|
### 파일 A: `frontend/lib/registry/pop-components/types.ts`
|
||||||
- [x] 커넥션 관리 리스트/모달 UI 수정
|
|
||||||
- [x] 연결 테스트 UI 수정 및 기능 확인
|
|
||||||
|
|
||||||
## 에러 처리 계획
|
#### 변경 A-1: CardFieldBinding 타입 확장
|
||||||
- **JSON 파싱 에러**: Body 입력값이 유효한 JSON이 아닐 경우 에러 처리
|
|
||||||
- **API 호출 에러**: 외부 API 호출 실패 시 상세 로그 기록 및 클라이언트에 에러 메시지 전달
|
|
||||||
- **SSL 인증 에러**: `rejectUnauthorized: false` 옵션으로 처리 (기존 `RestApiConnector` 활용)
|
|
||||||
|
|
||||||
## 진행 상태
|
**현재 코드** (라인 367~372):
|
||||||
- [완료] 모든 단계 구현 완료
|
```typescript
|
||||||
|
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`
|
||||||
|
|
||||||
|
#### 변경 B-1: 본문 필드 편집기(FieldEditor)에 값 유형 선택 추가
|
||||||
|
|
||||||
|
**현재**: 필드 편집 시 라벨, 컬럼, 텍스트색상만 설정 가능
|
||||||
|
|
||||||
|
**변경**: 값 유형 라디오("DB 컬럼" / "계산식") 추가
|
||||||
|
- "DB 컬럼" 선택 시: 기존 컬럼 Select 표시
|
||||||
|
- "계산식" 선택 시: 수식 입력란 + 사용 가능한 컬럼/변수 칩 목록 표시
|
||||||
|
- 사용 가능한 변수: DB 컬럼명들 + `$input` (입력 필드 활성화 시)
|
||||||
|
|
||||||
|
**하위 호환**: 기존 저장 데이터에 `valueType`이 없으면 `"column"`으로 기본 처리
|
||||||
|
|
||||||
|
#### 변경 B-2: 입력 필드 설정 섹션 개편
|
||||||
|
|
||||||
|
**현재 설정 항목**: 라벨, 단위, 기본값, 최소/최대, 최대값 컬럼, 저장 컬럼
|
||||||
|
|
||||||
|
**변경 설정 항목**:
|
||||||
|
```
|
||||||
|
라벨 [입고 수량 ]
|
||||||
|
단위 [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>
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
|
|
@ -1,195 +0,0 @@
|
||||||
# PLM 윈도우 개발환경 가이드
|
|
||||||
|
|
||||||
## 🚀 빠른 시작
|
|
||||||
|
|
||||||
### 1. 전체 환경 시작
|
|
||||||
```cmd
|
|
||||||
start-windows.bat
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 환경 중지
|
|
||||||
```cmd
|
|
||||||
stop-windows.bat
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 빌드만 실행
|
|
||||||
```cmd
|
|
||||||
build-windows.bat
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 사전 요구사항
|
|
||||||
|
|
||||||
### 필수 소프트웨어
|
|
||||||
- **Docker Desktop for Windows** (WSL2 백엔드 사용)
|
|
||||||
- **Java Development Kit (JDK) 7 이상**
|
|
||||||
- **Git for Windows**
|
|
||||||
|
|
||||||
### Docker Desktop 설정
|
|
||||||
1. Docker Desktop 설치
|
|
||||||
2. **Settings > General**에서 "Use WSL 2 based engine" 체크
|
|
||||||
3. **Settings > Resources > WSL Integration**에서 WSL 배포판 활성화
|
|
||||||
|
|
||||||
## 📁 파일 구조
|
|
||||||
|
|
||||||
```
|
|
||||||
new_ph/
|
|
||||||
├── start-windows.bat # 🎯 메인 시작 스크립트
|
|
||||||
├── stop-windows.bat # ⏹️ 중지 스크립트
|
|
||||||
├── build-windows.bat # 🔨 Java 빌드 스크립트
|
|
||||||
├── docker-compose.win.yml # 🐳 윈도우용 Docker Compose
|
|
||||||
├── Dockerfile.win # 🐳 윈도우용 Dockerfile
|
|
||||||
├── config.windows.env # ⚙️ 환경 변수 설정
|
|
||||||
└── README-WINDOWS.md # 📖 이 파일
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚙️ 환경 설정
|
|
||||||
|
|
||||||
### config.windows.env 파일 수정
|
|
||||||
```env
|
|
||||||
# 데이터베이스 설정
|
|
||||||
DB_PASSWORD=your_password_here
|
|
||||||
|
|
||||||
# 포트 설정 (충돌 시 변경)
|
|
||||||
TOMCAT_PORT=9090
|
|
||||||
|
|
||||||
# 메모리 설정
|
|
||||||
TOMCAT_MEMORY_MIN=512m
|
|
||||||
TOMCAT_MEMORY_MAX=1024m
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🐳 Docker 서비스
|
|
||||||
|
|
||||||
### 애플리케이션 서비스
|
|
||||||
- **컨테이너명**: plm-windows
|
|
||||||
- **포트**: 9090 → 8080
|
|
||||||
- **접속 URL**: http://localhost:9090
|
|
||||||
|
|
||||||
### 데이터베이스 서비스
|
|
||||||
- **컨테이너명**: plm-postgres-win
|
|
||||||
- **포트**: 5432 → 5432
|
|
||||||
- **데이터베이스**: plm
|
|
||||||
- **사용자**: postgres
|
|
||||||
- **패스워드**: ph0909!!
|
|
||||||
|
|
||||||
## 🔧 주요 명령어
|
|
||||||
|
|
||||||
### Docker 관리
|
|
||||||
```cmd
|
|
||||||
# 컨테이너 상태 확인
|
|
||||||
docker-compose -f docker-compose.win.yml ps
|
|
||||||
|
|
||||||
# 로그 확인
|
|
||||||
docker-compose -f docker-compose.win.yml logs -f
|
|
||||||
|
|
||||||
# 특정 서비스 로그
|
|
||||||
docker-compose -f docker-compose.win.yml logs -f plm-app
|
|
||||||
docker-compose -f docker-compose.win.yml logs -f plm-db
|
|
||||||
|
|
||||||
# 컨테이너 재시작
|
|
||||||
docker-compose -f docker-compose.win.yml restart plm-app
|
|
||||||
```
|
|
||||||
|
|
||||||
### 개발 작업
|
|
||||||
```cmd
|
|
||||||
# 빌드만 실행
|
|
||||||
build-windows.bat
|
|
||||||
|
|
||||||
# 컨테이너 재빌드
|
|
||||||
docker-compose -f docker-compose.win.yml up --build -d
|
|
||||||
|
|
||||||
# 데이터베이스 리셋
|
|
||||||
docker-compose -f docker-compose.win.yml down -v
|
|
||||||
docker-compose -f docker-compose.win.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🐛 문제 해결
|
|
||||||
|
|
||||||
### Docker Desktop 실행 안됨
|
|
||||||
```cmd
|
|
||||||
# Windows 서비스 확인
|
|
||||||
sc query com.docker.service
|
|
||||||
|
|
||||||
# WSL2 상태 확인
|
|
||||||
wsl --status
|
|
||||||
|
|
||||||
# Docker Desktop 재시작
|
|
||||||
taskkill /f /im "Docker Desktop.exe"
|
|
||||||
start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Java 컴파일 오류
|
|
||||||
```cmd
|
|
||||||
# Java 버전 확인
|
|
||||||
java -version
|
|
||||||
javac -version
|
|
||||||
|
|
||||||
# 클래스패스 문제 시 수동 빌드
|
|
||||||
javac -cp "WebContent\WEB-INF\lib\*" -d WebContent\WEB-INF\classes src\com\pms\**\*.java
|
|
||||||
```
|
|
||||||
|
|
||||||
### 포트 충돌
|
|
||||||
```cmd
|
|
||||||
# 포트 사용 확인
|
|
||||||
netstat -ano | findstr :9090
|
|
||||||
|
|
||||||
# 프로세스 종료
|
|
||||||
taskkill /PID <PID번호> /F
|
|
||||||
```
|
|
||||||
|
|
||||||
### 볼륨 권한 문제
|
|
||||||
```cmd
|
|
||||||
# Docker 볼륨 정리
|
|
||||||
docker volume prune -f
|
|
||||||
|
|
||||||
# WSL2 재시작
|
|
||||||
wsl --shutdown
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 모니터링
|
|
||||||
|
|
||||||
### 리소스 사용량
|
|
||||||
```cmd
|
|
||||||
# Docker 시스템 정보
|
|
||||||
docker system df
|
|
||||||
|
|
||||||
# 컨테이너 리소스 사용량
|
|
||||||
docker stats
|
|
||||||
|
|
||||||
# 로그 크기 확인
|
|
||||||
dir logs /s
|
|
||||||
```
|
|
||||||
|
|
||||||
### 헬스체크
|
|
||||||
```cmd
|
|
||||||
# 애플리케이션 상태
|
|
||||||
curl http://localhost:9090
|
|
||||||
|
|
||||||
# 데이터베이스 연결 테스트
|
|
||||||
docker exec plm-postgres-win psql -U postgres -d plm -c "SELECT version();"
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔄 업데이트
|
|
||||||
|
|
||||||
### 코드 변경 후
|
|
||||||
1. `build-windows.bat` 실행
|
|
||||||
2. `docker-compose -f docker-compose.win.yml restart plm-app`
|
|
||||||
|
|
||||||
### Docker 이미지 업데이트
|
|
||||||
```cmd
|
|
||||||
docker-compose -f docker-compose.win.yml down
|
|
||||||
docker-compose -f docker-compose.win.yml pull
|
|
||||||
docker-compose -f docker-compose.win.yml up --build -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📞 지원
|
|
||||||
|
|
||||||
문제가 발생하면 다음을 확인하세요:
|
|
||||||
|
|
||||||
1. **로그 파일**: `logs/` 디렉토리
|
|
||||||
2. **Docker 로그**: `docker-compose -f docker-compose.win.yml logs`
|
|
||||||
3. **시스템 요구사항**: Docker Desktop, WSL2, Java JDK
|
|
||||||
4. **네트워크**: 방화벽, 포트 충돌
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**🎉 즐거운 개발 되세요!**
|
|
||||||
519
README.md
|
|
@ -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에서는 완전히 새로운 사용자 경험을 제공합니다!**
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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`으로 마이그레이션 안 됨 | 낮음 | 이전에 작동 안 했으므로 실질 영향 없음 |
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
Manifest-Version: 1.0
|
|
||||||
Class-Path:
|
|
||||||
|
|
||||||
|
|
@ -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 ™</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>
|
|
||||||
|
|
@ -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}
|
|
||||||
|
|
@ -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}
|
|
||||||
|
|
@ -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}
|
|
||||||
|
|
@ -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}
|
|
||||||
|
Before Width: | Height: | Size: 115 B |
|
Before Width: | Height: | Size: 526 B |
|
Before Width: | Height: | Size: 331 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 159 B |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 103 B |
|
Before Width: | Height: | Size: 43 B |
|
Before Width: | Height: | Size: 56 B |
|
Before Width: | Height: | Size: 941 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 104 B |
|
Before Width: | Height: | Size: 139 B |
|
Before Width: | Height: | Size: 155 B |
|
Before Width: | Height: | Size: 270 B |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
|
@ -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";
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
};
|
|
||||||
|
|
@ -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
|
|
||||||
여기입니다! :)
|
|
||||||
|
|
@ -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 모드를 제공하여 텍스트만으로 본문의 내용을 작성할 수 있도록 편집 모드를 추가 제공한다.
|
|
||||||
|
|
@ -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™ 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('<img id="test" width="0" height="0">'))?>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
if(!document.getElementById("test")) {
|
|
||||||
alert("PHP가 실행되지 않았습니다. 내용을 로컬 파일로 전송한 것이 아니라 서버로 전송했는지 확인 해 주십시오.");
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -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;
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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);
|
|
||||||
// ./로컬경로에 파일 저장하기 ============================================
|
|
||||||
%>
|
|
||||||
|
|
@ -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);
|
|
||||||
?>
|
|
||||||
|
|
@ -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);
|
|
||||||
|
|
||||||
// ./로컬경로에 파일 저장하기 ============================================
|
|
||||||
%>
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 718 B |
|
Before Width: | Height: | Size: 553 B |
|
Before Width: | Height: | Size: 553 B |
|
Before Width: | Height: | Size: 157 B |
|
Before Width: | Height: | Size: 506 B |
|
|
@ -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);
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Smart Editor™ WYSIWYG Mode</title>
|
|
||||||
</head>
|
|
||||||
<body class="se2_inputarea" style="height:0;-webkit-nbsp-mode:normal"></body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -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™ WYSIWYG Mode</title>
|
|
||||||
</head>
|
|
||||||
<body class="se2_inputarea" style="height:0"></body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
||||||