Compare commits
No commits in common. "main" and "feature/inbound-inventory-fix" have entirely different histories.
main
...
feature/in
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="output" path="WebContent/WEB-INF/classes"/>
|
||||
</classpath>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"Framelink Figma MCP": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "figma-developer-mcp", "--figma-api-key=figd_NrYdIWf-CnC23NyH6eMym7sBdfbZTuXyS91tI3VS", "--stdio"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
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)
|
||||
|
|
@ -3,143 +3,205 @@ description:
|
|||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 데이터베이스 가이드
|
||||
|
||||
## 데이터베이스 설정
|
||||
|
||||
### PostgreSQL 연결
|
||||
- **드라이버**: `pg` (node-postgres)
|
||||
- **설정 파일**: `backend-node/src/config/database.ts`
|
||||
- **환경 변수**: `backend-node/.env` (DATABASE_URL)
|
||||
- **JNDI 리소스명**: `plm`
|
||||
- **드라이버**: `org.postgresql.Driver`
|
||||
- **설정 파일**: [context.xml](mdc:tomcat-conf/context.xml)
|
||||
|
||||
### 연결 풀 사용
|
||||
```typescript
|
||||
import { Pool } from 'pg';
|
||||
### 초기 데이터
|
||||
- **스키마 파일**: [ilshin.pgsql](mdc:db/ilshin.pgsql)
|
||||
- **역할 설정**: [00-create-roles.sh](mdc:db/00-create-roles.sh)
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
});
|
||||
```
|
||||
## MyBatis 설정
|
||||
|
||||
## 쿼리 패턴
|
||||
|
||||
### 기본 CRUD
|
||||
```typescript
|
||||
// SELECT
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM example_table WHERE company_code = $1 ORDER BY created_at DESC',
|
||||
[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]
|
||||
);
|
||||
### SqlSession 사용 패턴
|
||||
```java
|
||||
public List<Map<String, Object>> getData(Map<String, Object> paramMap) {
|
||||
SqlSession sqlSession = SqlMapConfig.getInstance().getSqlSession();
|
||||
try {
|
||||
return sqlSession.selectList("namespace.queryId", paramMap);
|
||||
} finally {
|
||||
sqlSession.close(); // 반드시 리소스 해제
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 트랜잭션 처리
|
||||
```typescript
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
await client.query('INSERT INTO ...', [params]);
|
||||
await client.query('UPDATE ...', [params]);
|
||||
await client.query('COMMIT');
|
||||
} catch (e) {
|
||||
await client.query('ROLLBACK');
|
||||
```java
|
||||
public void saveData(Map<String, Object> paramMap) {
|
||||
SqlSession sqlSession = SqlMapConfig.getInstance().getSqlSession(false); // autoCommit=false
|
||||
try {
|
||||
sqlSession.insert("namespace.insertQuery", paramMap);
|
||||
sqlSession.update("namespace.updateQuery", paramMap);
|
||||
sqlSession.commit(); // 명시적 커밋
|
||||
} catch (Exception e) {
|
||||
sqlSession.rollback(); // 오류 시 롤백
|
||||
throw e;
|
||||
} finally {
|
||||
client.release();
|
||||
} finally {
|
||||
sqlSession.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 동적 조건 처리
|
||||
```typescript
|
||||
const conditions: string[] = ['company_code = $1'];
|
||||
const params: any[] = [companyCode];
|
||||
let paramIndex = 2;
|
||||
## 매퍼 XML 작성 가이드
|
||||
|
||||
if (searchText) {
|
||||
conditions.push(`name ILIKE $${paramIndex}`);
|
||||
params.push(`%${searchText}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
### 기본 구조
|
||||
```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="admin">
|
||||
<!-- 쿼리 정의 -->
|
||||
</mapper>
|
||||
```
|
||||
|
||||
if (status) {
|
||||
conditions.push(`status = $${paramIndex}`);
|
||||
params.push(status);
|
||||
paramIndex++;
|
||||
}
|
||||
### 파라미터 바인딩
|
||||
```xml
|
||||
<!-- 안전한 파라미터 바인딩 (권장) -->
|
||||
<select id="selectUser" parameterType="map" resultType="map">
|
||||
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`;
|
||||
const result = await pool.query(query, params);
|
||||
<!-- 동적 조건 처리 -->
|
||||
<select id="selectUserList" parameterType="map" resultType="map">
|
||||
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_auth_group**: 메뉴 권한 그룹
|
||||
- **auth_group**: 권한 그룹 정보
|
||||
- **MENU_INFO**: 메뉴 정보
|
||||
- **MENU_AUTH_GROUP**: 메뉴 권한 그룹
|
||||
- **AUTH_GROUP**: 권한 그룹 정보
|
||||
|
||||
### 사용자 관리
|
||||
- **user_info**: 사용자 정보
|
||||
- **dept_info**: 부서 정보
|
||||
- **user_auth**: 사용자 권한
|
||||
- **USER_INFO**: 사용자 정보
|
||||
- **DEPT_INFO**: 부서 정보
|
||||
- **USER_AUTH**: 사용자 권한
|
||||
|
||||
### 코드 관리
|
||||
- **code_info**: 공통 코드
|
||||
- **code_category**: 코드 카테고리
|
||||
- **CODE_INFO**: 공통 코드
|
||||
- **CODE_CATEGORY**: 코드 카테고리
|
||||
|
||||
## 마이그레이션
|
||||
## 데이터베이스 개발 모범 사례
|
||||
|
||||
마이그레이션 파일은 `db/migrations/` 디렉토리에 순번으로 관리:
|
||||
### 1. 파라미터 검증
|
||||
```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/
|
||||
├── 001_initial_schema.sql
|
||||
├── 002_add_company_code.sql
|
||||
├── ...
|
||||
└── 1021_create_numbering_audit_log.sql
|
||||
|
||||
### 2. 페이징 처리
|
||||
```xml
|
||||
<select id="selectListWithPaging" parameterType="map" resultType="map">
|
||||
SELECT * FROM (
|
||||
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
|
||||
CREATE INDEX idx_example_company_code ON example_table(company_code);
|
||||
-- 자주 검색되는 컬럼에 인덱스 생성
|
||||
CREATE INDEX idx_user_dept ON user_info(dept_code);
|
||||
CREATE INDEX idx_menu_parent ON menu_info(parent_id);
|
||||
```
|
||||
|
||||
### 페이징 처리
|
||||
```typescript
|
||||
const query = `
|
||||
SELECT * FROM example_table
|
||||
WHERE company_code = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`;
|
||||
const result = await pool.query(query, [companyCode, limit, offset]);
|
||||
### 쿼리 최적화
|
||||
```xml
|
||||
<!-- EXISTS 사용으로 성능 개선 -->
|
||||
<select id="selectUserWithAuth" resultType="map">
|
||||
SELECT u.* FROM user_info u
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM user_auth ua
|
||||
WHERE ua.user_id = u.user_id
|
||||
AND ua.auth_code = #{authCode}
|
||||
)
|
||||
</select>
|
||||
```
|
||||
|
||||
### 슬로우 쿼리 확인
|
||||
```sql
|
||||
SELECT query, mean_time, calls
|
||||
FROM pg_stat_statements
|
||||
ORDER BY mean_time DESC;
|
||||
### 배치 처리
|
||||
```xml
|
||||
<!-- 대량 데이터 삽입 시 배치 사용 -->
|
||||
<insert id="insertBatch" parameterType="list">
|
||||
INSERT INTO table_name (col1, col2, col3)
|
||||
VALUES
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(#{item.col1}, #{item.col2}, #{item.col3})
|
||||
</foreach>
|
||||
</insert>
|
||||
```
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
---
|
||||
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 배포
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
# inputType 사용 가이드
|
||||
|
||||
## 핵심 원칙
|
||||
|
||||
**컬럼 타입 판단 시 반드시 `inputType`을 사용해야 합니다. `webType`은 레거시이며 더 이상 사용하지 않습니다.**
|
||||
|
||||
---
|
||||
|
||||
## 올바른 사용법
|
||||
|
||||
### ✅ inputType 사용 (권장)
|
||||
|
||||
```typescript
|
||||
// 카테고리 타입 체크
|
||||
if (columnMeta.inputType === "category") {
|
||||
// 카테고리 처리 로직
|
||||
}
|
||||
|
||||
// 코드 타입 체크
|
||||
if (meta.inputType === "code") {
|
||||
// 코드 처리 로직
|
||||
}
|
||||
|
||||
// 필터링
|
||||
const categoryColumns = Object.entries(columnMeta)
|
||||
.filter(([_, meta]) => meta.inputType === "category")
|
||||
.map(([columnName, _]) => columnName);
|
||||
```
|
||||
|
||||
### ❌ webType 사용 (금지)
|
||||
|
||||
```typescript
|
||||
// ❌ 절대 사용 금지!
|
||||
if (columnMeta.webType === "category") { ... }
|
||||
|
||||
// ❌ 이것도 금지!
|
||||
const categoryColumns = columns.filter(col => col.webType === "category");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API에서 inputType 가져오기
|
||||
|
||||
### Backend API
|
||||
|
||||
```typescript
|
||||
// 컬럼 입력 타입 정보 가져오기
|
||||
const inputTypes = await tableTypeApi.getColumnInputTypes(tableName);
|
||||
|
||||
// inputType 맵 생성
|
||||
const inputTypeMap: Record<string, string> = {};
|
||||
inputTypes.forEach((col: any) => {
|
||||
inputTypeMap[col.columnName] = col.inputType;
|
||||
});
|
||||
```
|
||||
|
||||
### columnMeta 구조
|
||||
|
||||
```typescript
|
||||
interface ColumnMeta {
|
||||
webType?: string; // 레거시, 사용 금지
|
||||
codeCategory?: string;
|
||||
inputType?: string; // ✅ 반드시 이것 사용!
|
||||
}
|
||||
|
||||
const columnMeta: Record<string, ColumnMeta> = {
|
||||
material: {
|
||||
webType: "category", // 무시
|
||||
codeCategory: "",
|
||||
inputType: "category", // ✅ 이것만 사용
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 캐시 사용 시 주의사항
|
||||
|
||||
### ❌ 잘못된 캐시 처리 (inputType 누락)
|
||||
|
||||
```typescript
|
||||
const cached = tableColumnCache.get(cacheKey);
|
||||
if (cached) {
|
||||
const meta: Record<string, ColumnMeta> = {};
|
||||
|
||||
cached.columns.forEach((col: any) => {
|
||||
meta[col.columnName] = {
|
||||
webType: col.webType,
|
||||
codeCategory: col.codeCategory,
|
||||
// ❌ inputType 누락!
|
||||
};
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ 올바른 캐시 처리 (inputType 포함)
|
||||
|
||||
```typescript
|
||||
const cached = tableColumnCache.get(cacheKey);
|
||||
if (cached) {
|
||||
const meta: Record<string, ColumnMeta> = {};
|
||||
|
||||
// 캐시된 inputTypes 맵 생성
|
||||
const inputTypeMap: Record<string, string> = {};
|
||||
if (cached.inputTypes) {
|
||||
cached.inputTypes.forEach((col: any) => {
|
||||
inputTypeMap[col.columnName] = col.inputType;
|
||||
});
|
||||
}
|
||||
|
||||
cached.columns.forEach((col: any) => {
|
||||
meta[col.columnName] = {
|
||||
webType: col.webType,
|
||||
codeCategory: col.codeCategory,
|
||||
inputType: inputTypeMap[col.columnName], // ✅ inputType 포함!
|
||||
};
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 주요 inputType 종류
|
||||
|
||||
| inputType | 설명 | 사용 예시 |
|
||||
| ---------- | ---------------- | ------------------ |
|
||||
| `text` | 일반 텍스트 입력 | 이름, 설명 등 |
|
||||
| `number` | 숫자 입력 | 금액, 수량 등 |
|
||||
| `date` | 날짜 입력 | 생성일, 수정일 등 |
|
||||
| `datetime` | 날짜+시간 입력 | 타임스탬프 등 |
|
||||
| `category` | 카테고리 선택 | 분류, 상태 등 |
|
||||
| `code` | 공통 코드 선택 | 코드 마스터 데이터 |
|
||||
| `boolean` | 예/아니오 | 활성화 여부 등 |
|
||||
| `email` | 이메일 입력 | 이메일 주소 |
|
||||
| `url` | URL 입력 | 웹사이트 주소 |
|
||||
| `image` | 이미지 업로드 | 프로필 사진 등 |
|
||||
| `file` | 파일 업로드 | 첨부파일 등 |
|
||||
|
||||
---
|
||||
|
||||
## 실제 적용 사례
|
||||
|
||||
### 1. TableListComponent - 카테고리 매핑 로드
|
||||
|
||||
```typescript
|
||||
// ✅ inputType으로 카테고리 컬럼 필터링
|
||||
const categoryColumns = Object.entries(columnMeta)
|
||||
.filter(([_, meta]) => meta.inputType === "category")
|
||||
.map(([columnName, _]) => columnName);
|
||||
|
||||
// 각 카테고리 컬럼의 값 목록 조회
|
||||
for (const columnName of categoryColumns) {
|
||||
const response = await apiClient.get(
|
||||
`/table-categories/${tableName}/${columnName}/values`
|
||||
);
|
||||
// 매핑 처리...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. InteractiveDataTable - 셀 값 렌더링
|
||||
|
||||
```typescript
|
||||
// ✅ inputType으로 렌더링 분기
|
||||
const inputType = columnMeta[column.columnName]?.inputType;
|
||||
|
||||
switch (inputType) {
|
||||
case "category":
|
||||
// 카테고리 배지 렌더링
|
||||
return <Badge>{categoryLabel}</Badge>;
|
||||
|
||||
case "code":
|
||||
// 코드명 표시
|
||||
return codeName;
|
||||
|
||||
case "date":
|
||||
// 날짜 포맷팅
|
||||
return formatDate(value);
|
||||
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 검색 필터 생성
|
||||
|
||||
```typescript
|
||||
// ✅ inputType에 따라 다른 검색 UI 제공
|
||||
const renderSearchInput = (column: ColumnConfig) => {
|
||||
const inputType = columnMeta[column.columnName]?.inputType;
|
||||
|
||||
switch (inputType) {
|
||||
case "category":
|
||||
return <CategorySelect column={column} />;
|
||||
|
||||
case "code":
|
||||
return <CodeSelect column={column} />;
|
||||
|
||||
case "date":
|
||||
return <DateRangePicker column={column} />;
|
||||
|
||||
case "number":
|
||||
return <NumberRangeInput column={column} />;
|
||||
|
||||
default:
|
||||
return <TextInput column={column} />;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 마이그레이션 체크리스트
|
||||
|
||||
기존 코드에서 `webType`을 `inputType`으로 전환할 때:
|
||||
|
||||
- [ ] `webType` 참조를 모두 `inputType`으로 변경
|
||||
- [ ] API 호출 시 `getColumnInputTypes()` 포함 확인
|
||||
- [ ] 캐시 사용 시 `cached.inputTypes` 매핑 확인
|
||||
- [ ] 타입 정의에서 `inputType` 필드 포함
|
||||
- [ ] 조건문에서 `inputType` 체크로 변경
|
||||
- [ ] 테스트 실행하여 정상 동작 확인
|
||||
|
||||
---
|
||||
|
||||
## 디버깅 팁
|
||||
|
||||
### inputType이 undefined인 경우
|
||||
|
||||
```typescript
|
||||
// 디버깅 로그 추가
|
||||
console.log("columnMeta:", columnMeta);
|
||||
console.log("inputType:", columnMeta[columnName]?.inputType);
|
||||
|
||||
// 체크 포인트:
|
||||
// 1. getColumnInputTypes() 호출 확인
|
||||
// 2. inputTypeMap 생성 확인
|
||||
// 3. meta 객체에 inputType 할당 확인
|
||||
// 4. 캐시 사용 시 cached.inputTypes 확인
|
||||
```
|
||||
|
||||
### webType만 있고 inputType이 없는 경우
|
||||
|
||||
```typescript
|
||||
// ❌ 잘못된 데이터 구조
|
||||
{
|
||||
material: {
|
||||
webType: "category",
|
||||
codeCategory: "",
|
||||
// inputType 누락!
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 올바른 데이터 구조
|
||||
{
|
||||
material: {
|
||||
webType: "category", // 레거시, 무시됨
|
||||
codeCategory: "",
|
||||
inputType: "category" // ✅ 필수!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 참고 자료
|
||||
|
||||
- **컴포넌트**: `/frontend/lib/registry/components/table-list/TableListComponent.tsx`
|
||||
- **API 클라이언트**: `/frontend/lib/api/tableType.ts`
|
||||
- **타입 정의**: `/frontend/types/table.ts`
|
||||
|
||||
---
|
||||
|
||||
## 요약
|
||||
|
||||
1. **항상 `inputType` 사용**, `webType` 사용 금지
|
||||
2. **API에서 `getColumnInputTypes()` 호출** 필수
|
||||
3. **캐시 사용 시 `inputTypes` 포함** 확인
|
||||
4. **디버깅 시 `inputType` 값 확인**
|
||||
5. **기존 코드 마이그레이션** 시 체크리스트 활용
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
---
|
||||
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 응답 속도 최적화
|
||||
- 클라이언트 사이드 캐싱
|
||||
- 이미지 및 정적 자원 최적화
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
---
|
||||
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,69 +8,30 @@ alwaysApply: true
|
|||
|
||||
## 프로젝트 정보
|
||||
|
||||
이 프로젝트는 WACE ERP/PLM 솔루션입니다.
|
||||
Node.js + Next.js 기반의 풀스택 웹 애플리케이션으로, 멀티테넌시를 지원합니다.
|
||||
이 프로젝트는 제품 수명 주기 관리(PLM - Product Lifecycle Management) 솔루션입니다.
|
||||
Spring Framework 기반의 Java 웹 애플리케이션으로, 제품 개발부터 폐기까지의 전체 생명주기를 관리합니다.
|
||||
|
||||
## 기술 스택
|
||||
|
||||
- **Backend**: Node.js 20+, Express 4, TypeScript
|
||||
- **Frontend**: Next.js (App Router, Turbopack), React, shadcn/ui, Tailwind CSS
|
||||
- **Database**: PostgreSQL (pg 드라이버 직접 사용)
|
||||
- **인증**: JWT (jsonwebtoken)
|
||||
- **빌드**: 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/ # 프로젝트 문서
|
||||
```
|
||||
- **Backend**: Java 7, Spring Framework 3.2.4, MyBatis 3.2.3
|
||||
- **Frontend**: JSP, jQuery 1.11.3/2.1.4, jqGrid 4.7.1
|
||||
- **Database**: PostgreSQL
|
||||
- **WAS**: Apache Tomcat 7.0
|
||||
- **Build**: Eclipse IDE 기반 (Maven/Gradle 미사용)
|
||||
|
||||
## 주요 기능
|
||||
|
||||
- 사용자 및 권한 관리 (멀티테넌시)
|
||||
- 메뉴 및 화면 관리
|
||||
- 플로우(워크플로우) 관리
|
||||
- BOM 관리
|
||||
- 문서/파일 관리
|
||||
- 화면 디자이너 (동적 화면 생성)
|
||||
- 메일 연동
|
||||
- 외부 DB 연결
|
||||
|
||||
## 개발 환경
|
||||
|
||||
```bash
|
||||
# 백엔드 (nodemon으로 자동 재시작)
|
||||
cd backend-node && npm run dev
|
||||
|
||||
# 프론트엔드 (Turbopack)
|
||||
cd frontend && npm run dev
|
||||
```
|
||||
- 제품 정보 관리
|
||||
- BOM (Bill of Materials) 관리
|
||||
- 설계 변경 관리 (ECO/ECR)
|
||||
- 문서 관리 및 버전 제어
|
||||
- 프로젝트/일정 관리
|
||||
- 사용자 및 권한 관리
|
||||
- 워크플로우 관리
|
||||
|
||||
## 주요 설정 파일
|
||||
|
||||
- `backend-node/.env` - 백엔드 환경 변수 (DB, JWT 등)
|
||||
- `frontend/.env.local` - 프론트엔드 환경 변수
|
||||
- `docker/` - Docker Compose 설정 (dev/prod)
|
||||
- `Dockerfile` - 프로덕션 멀티스테이지 빌드
|
||||
- `Jenkinsfile` - CI/CD 파이프라인
|
||||
- [web.xml](mdc:WebContent/WEB-INF/web.xml) - 웹 애플리케이션 배포 설정
|
||||
- [dispatcher-servlet.xml](mdc:WebContent/WEB-INF/dispatcher-servlet.xml) - Spring MVC 설정
|
||||
- [docker-compose.dev.yml](mdc:docker-compose.dev.yml) - 개발환경 Docker 설정
|
||||
- [docker-compose.prod.yml](mdc:docker-compose.prod.yml) - 운영환경 Docker 설정
|
||||
|
|
|
|||
|
|
@ -3,124 +3,246 @@ description:
|
|||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 보안 가이드
|
||||
|
||||
## 인증 및 인가
|
||||
|
||||
### JWT 기반 인증
|
||||
현재 시스템은 JWT 토큰 기반 인증을 사용합니다:
|
||||
### 세션 기반 인증
|
||||
현재 시스템은 세션 기반 인증을 사용합니다:
|
||||
|
||||
```typescript
|
||||
// 토큰 생성 (로그인 시)
|
||||
const token = jwt.sign(
|
||||
{ userId, companyCode, role },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
|
||||
);
|
||||
```java
|
||||
// 사용자 인증 정보 저장
|
||||
PersonBean person = new PersonBean();
|
||||
person.setUserId(userId);
|
||||
person.setUserName(userName);
|
||||
request.getSession().setAttribute(Constants.PERSON_BEAN, person);
|
||||
|
||||
// 토큰 검증 (미들웨어)
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
req.user = decoded;
|
||||
// 인증 정보 조회
|
||||
PersonBean person = (PersonBean)request.getSession().getAttribute(Constants.PERSON_BEAN);
|
||||
if (person == null) {
|
||||
// 로그인 페이지로 리다이렉트
|
||||
}
|
||||
```
|
||||
|
||||
### 권한 관리 구조
|
||||
- **auth_group**: 권한 그룹 정의
|
||||
- **menu_auth_group**: 메뉴별 권한 그룹 매핑
|
||||
- **user_auth**: 사용자별 권한 할당
|
||||
- **AUTH_GROUP**: 권한 그룹 정의
|
||||
- **MENU_AUTH_GROUP**: 메뉴별 권한 그룹 매핑
|
||||
- **USER_AUTH**: 사용자별 권한 할당
|
||||
|
||||
### 인증 미들웨어
|
||||
`backend-node/src/middleware/authMiddleware.ts` 참조
|
||||
### 메뉴 접근 권한 체크
|
||||
```java
|
||||
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 인젝션 방지
|
||||
**올바른 방법** - 파라미터 바인딩 사용:
|
||||
```typescript
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM user_info WHERE user_id = $1',
|
||||
[userId]
|
||||
);
|
||||
```xml
|
||||
<select id="selectUser" parameterType="map" resultType="map">
|
||||
SELECT * FROM user_info
|
||||
WHERE user_id = #{userId} <!-- 안전한 파라미터 바인딩 -->
|
||||
</select>
|
||||
```
|
||||
|
||||
**위험한 방법** - 직접 문자열 삽입:
|
||||
```typescript
|
||||
const result = await pool.query(
|
||||
`SELECT * FROM user_info WHERE user_id = '${userId}'`
|
||||
);
|
||||
**위험한 방법** - 직접 문자열 치환:
|
||||
```xml
|
||||
<select id="selectUser" parameterType="map" resultType="map">
|
||||
SELECT * FROM user_info
|
||||
WHERE user_id = '${userId}' <!-- SQL 인젝션 위험 -->
|
||||
</select>
|
||||
```
|
||||
|
||||
### 패스워드 보안
|
||||
```typescript
|
||||
import bcrypt from 'bcryptjs';
|
||||
```java
|
||||
// 패스워드 암호화 (EncryptUtil 사용)
|
||||
String encryptedPassword = EncryptUtil.encrypt(plainPassword);
|
||||
|
||||
// 해싱
|
||||
const hashedPassword = await bcrypt.hash(plainPassword, 10);
|
||||
|
||||
// 검증
|
||||
const isValid = await bcrypt.compare(plainPassword, hashedPassword);
|
||||
// 패스워드 검증
|
||||
boolean isValid = EncryptUtil.matches(plainPassword, encryptedPassword);
|
||||
```
|
||||
|
||||
### 입력값 검증
|
||||
```typescript
|
||||
import Joi from 'joi';
|
||||
|
||||
const schema = Joi.object({
|
||||
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 });
|
||||
```java
|
||||
// CommonUtils를 사용한 입력값 정제
|
||||
String safeInput = CommonUtils.checkNull(request.getParameter("input"));
|
||||
if (StringUtils.isEmpty(safeInput)) {
|
||||
throw new IllegalArgumentException("필수 입력값이 누락되었습니다.");
|
||||
}
|
||||
```
|
||||
|
||||
## 보안 미들웨어
|
||||
## 세션 보안
|
||||
|
||||
### Helmet (보안 헤더)
|
||||
```typescript
|
||||
import helmet from 'helmet';
|
||||
app.use(helmet());
|
||||
### 세션 설정
|
||||
[web.xml](mdc:WebContent/WEB-INF/web.xml)에서 세션 타임아웃 설정:
|
||||
```xml
|
||||
<session-config>
|
||||
<session-timeout>1440</session-timeout> <!-- 24시간 -->
|
||||
</session-config>
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
```typescript
|
||||
import rateLimit from 'express-rate-limit';
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 100,
|
||||
});
|
||||
app.use('/api/', limiter);
|
||||
### 세션 무효화
|
||||
```java
|
||||
// 로그아웃 시 세션 무효화
|
||||
@RequestMapping("/logout.do")
|
||||
public String logout(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session != null) {
|
||||
session.invalidate();
|
||||
}
|
||||
return "redirect:/login.do";
|
||||
}
|
||||
```
|
||||
|
||||
### CORS 설정
|
||||
```typescript
|
||||
import cors from 'cors';
|
||||
app.use(cors({
|
||||
origin: process.env.CORS_ORIGIN,
|
||||
credentials: true,
|
||||
}));
|
||||
## 파일 업로드 보안
|
||||
|
||||
### 파일 확장자 검증
|
||||
```java
|
||||
public boolean isAllowedFileType(String fileName) {
|
||||
String[] allowedExtensions = {".jpg", ".jpeg", ".png", ".gif", ".pdf", ".doc", ".docx", ".xls", ".xlsx"};
|
||||
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 인젝션 방지 ($1 파라미터 바인딩 사용)
|
||||
- [ ] XSS 방지 (React 자동 이스케이프 활용)
|
||||
- [ ] 입력값 검증 (Joi 스키마)
|
||||
- [ ] 패스워드 bcrypt 해싱
|
||||
- [ ] JWT 토큰 만료 설정
|
||||
- [ ] SQL 인젝션 방지 (#{} 파라미터 바인딩 사용)
|
||||
- [ ] XSS 방지 (출력값 이스케이프)
|
||||
- [ ] CSRF 방지 (토큰 검증)
|
||||
- [ ] 입력값 검증 및 정제
|
||||
- [ ] 패스워드 암호화 저장
|
||||
|
||||
### 설정 레벨
|
||||
- [ ] Helmet 보안 헤더
|
||||
- [ ] Rate Limiting 적용
|
||||
- [ ] CORS 적절한 설정
|
||||
- [ ] 세션 타임아웃 설정
|
||||
- [ ] 파일 업로드 제한
|
||||
- [ ] 오류 페이지 설정
|
||||
- [ ] HTTPS 사용 (운영 환경)
|
||||
- [ ] 환경 변수로 시크릿 관리
|
||||
- [ ] 보안 헤더 설정
|
||||
|
||||
### 운영 레벨
|
||||
- [ ] winston 로깅 모니터링
|
||||
- [ ] 정기적인 보안 점검
|
||||
- [ ] 로그 모니터링
|
||||
- [ ] 권한 정기 검토
|
||||
- [ ] 패스워드 정책 적용
|
||||
- [ ] 백업 데이터 암호화
|
||||
|
|
@ -20,7 +20,7 @@ CREATE TABLE "테이블명" (
|
|||
-- 시스템 기본 컬럼 (자동 포함)
|
||||
"id" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
||||
"created_date" timestamp DEFAULT now(),
|
||||
"updated_date" timestamp DEFAULT now(),b
|
||||
"updated_date" timestamp DEFAULT now(),
|
||||
"writer" varchar(500) DEFAULT NULL,
|
||||
"company_code" varchar(500),
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,166 @@
|
|||
---
|
||||
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,48 +1,5 @@
|
|||
# Cursor Rules for ERP-node Project
|
||||
|
||||
## 🚨 비즈니스 로직 요청 양식 검증 (필수)
|
||||
|
||||
**사용자가 화면 개발 또는 비즈니스 로직 구현을 요청할 때, 아래 양식을 따르지 않으면 반드시 다음과 같이 응답하세요:**
|
||||
|
||||
```
|
||||
안녕하세요. Oh My Master! 양식을 못 알아 듣겠습니다.
|
||||
다시 한번 작성해주십쇼.
|
||||
=== 비즈니스 로직 요청서 ===
|
||||
|
||||
【화면 정보】
|
||||
- 화면명:
|
||||
- 회사코드:
|
||||
- 메뉴ID (있으면):
|
||||
|
||||
【테이블 정보】
|
||||
- 메인 테이블:
|
||||
- 디테일 테이블 (있으면):
|
||||
- 관계 FK (있으면):
|
||||
|
||||
【버튼 목록】
|
||||
버튼1:
|
||||
- 버튼명:
|
||||
- 동작 유형: (저장/삭제/수정/조회/기타)
|
||||
- 조건 (있으면):
|
||||
- 대상 테이블:
|
||||
- 추가 동작 (있으면):
|
||||
|
||||
【추가 요구사항】
|
||||
-
|
||||
```
|
||||
|
||||
**양식 미준수 판단 기준:**
|
||||
1. "화면 만들어줘" 같이 테이블명/버튼 정보 없이 요청
|
||||
2. "저장하면 저장해줘" 같이 구체적인 테이블/로직 설명 없음
|
||||
3. "이전이랑 비슷하게" 같이 모호한 참조
|
||||
4. 버튼별 조건/동작이 명시되지 않음
|
||||
|
||||
**양식 미준수 시 절대 작업 진행하지 말고, 위 양식을 보여주며 다시 작성하라고 요청하세요.**
|
||||
|
||||
**상세 가이드**: [화면개발_표준_가이드.md](docs/screen-implementation-guide/화면개발_표준_가이드.md)
|
||||
|
||||
---
|
||||
|
||||
## 🚨 최우선 보안 규칙: 멀티테넌시
|
||||
|
||||
**모든 코드 작성/수정 완료 후 반드시 다음 파일을 확인하세요:**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
# 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,12 +1,5 @@
|
|||
# Claude Code
|
||||
.claude/
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
bower_components
|
||||
|
||||
# Package manager logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
|
@ -17,125 +10,138 @@ pids
|
|||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.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
|
||||
*.tsbuildinfo
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
build/Release
|
||||
|
||||
# Cache
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
.cache/
|
||||
.parcel-cache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
*.cache
|
||||
cache/
|
||||
|
||||
# Next.js
|
||||
.next
|
||||
|
||||
# Storybook
|
||||
.out
|
||||
.storybook-out
|
||||
|
||||
# REPL history
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Package archives
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# Temporary
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
log/
|
||||
*.log
|
||||
|
||||
# ===== 환경 변수 및 민감 정보 =====
|
||||
|
||||
# 환경 변수
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
.env.local
|
||||
.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
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
docker-compose.prod.yml
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# 설정 파일
|
||||
configs/
|
||||
settings/
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# 키/인증서
|
||||
*.key
|
||||
*.pem
|
||||
*.p12
|
||||
*.pfx
|
||||
*.crt
|
||||
*.cert
|
||||
secrets/
|
||||
secrets.json
|
||||
secrets.yaml
|
||||
secrets.yml
|
||||
api-keys.json
|
||||
tokens.json
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# 데이터베이스 덤프
|
||||
*.sql
|
||||
*.dump
|
||||
db/dump/
|
||||
db/backup/
|
||||
# Build cache
|
||||
.cache/
|
||||
|
||||
# 백업
|
||||
*.bak
|
||||
*.backup
|
||||
*.old
|
||||
backup/
|
||||
# Storybook build outputs
|
||||
.out
|
||||
.storybook-out
|
||||
|
||||
# ===== IDE =====
|
||||
# Temporary folders
|
||||
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/
|
||||
.idea/
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.project
|
||||
.classpath
|
||||
.settings/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
*.user
|
||||
|
||||
# OS
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
|
|
@ -144,7 +150,127 @@ backup/
|
|||
ehthumbs.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/
|
||||
uploads/
|
||||
*.jpg
|
||||
|
|
@ -160,20 +286,4 @@ uploads/
|
|||
*.hwp
|
||||
*.hwpx
|
||||
|
||||
# ===== 기타 =====
|
||||
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
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 329 KiB |
|
Before Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
|
@ -0,0 +1,47 @@
|
|||
<?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>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
eclipse.preferences.version=1
|
||||
encoding//WebContent/WEB-INF/view/materMgmt/materOrderDown1.jsp=UTF-8
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
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
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?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>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
org.eclipse.wst.jsdt.launching.baseBrowserLibrary
|
||||
|
|
@ -0,0 +1 @@
|
|||
Window
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
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
|
||||
|
|
@ -377,7 +377,7 @@ docker-compose -f docker-compose.backend.mac.yml up --build -d
|
|||
|
||||
- Node.js 백엔드: `backend-node/` 디렉토리
|
||||
- Next.js 프론트엔드: `frontend/` 디렉토리
|
||||
- 데이터베이스: PostgreSQL (pg 드라이버)
|
||||
- 데이터베이스: PostgreSQL (JNDI 설정)
|
||||
|
||||
---
|
||||
|
||||
373
PLAN.MD
|
|
@ -1,337 +1,36 @@
|
|||
# 현재 구현 계획: pop-card-list 입력 필드/계산 필드 구조 개편
|
||||
|
||||
> **작성일**: 2026-02-24
|
||||
> **상태**: 계획 완료, 코딩 대기
|
||||
> **목적**: 입력 필드 설정 단순화 + 본문 필드에 계산식 통합 + 기존 계산 필드 섹션 제거
|
||||
|
||||
---
|
||||
|
||||
## 1. 변경 개요
|
||||
|
||||
### 배경
|
||||
- 기존: "입력 필드", "계산 필드", "담기 버튼" 3개가 별도 섹션으로 분리
|
||||
- 문제: 계산 필드가 본문 필드와 동일한 위치에 표시되어야 하는데 별도 영역에 있음
|
||||
- 문제: 입력 필드의 min/max 고정값은 비실용적 (실제로는 DB 컬럼 기준 제한이 필요)
|
||||
- 문제: step, columnName, sourceColumns, resultColumn 등 죽은 코드 존재
|
||||
|
||||
### 목표
|
||||
1. **본문 필드에 계산식 지원 추가** - 필드별로 "DB 컬럼" 또는 "계산식" 선택
|
||||
2. **입력 필드 설정 단순화** - 고정 min/max 제거, 제한 기준 컬럼 방식으로 변경
|
||||
3. **기존 "계산 필드" 섹션 제거** - 본문 필드에 통합되므로 불필요
|
||||
4. **죽은 코드 정리**
|
||||
|
||||
---
|
||||
|
||||
## 2. 수정 대상 파일 (3개)
|
||||
|
||||
### 파일 A: `frontend/lib/registry/pop-components/types.ts`
|
||||
|
||||
#### 변경 A-1: CardFieldBinding 타입 확장
|
||||
|
||||
**현재 코드** (라인 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>
|
||||
# 프로젝트: 외부 REST API 커넥션 관리 확장 (POST/Body 지원)
|
||||
|
||||
## 개요
|
||||
현재 GET 방식 위주로 구현된 외부 REST API 커넥션 관리 기능을 확장하여, POST, PUT, DELETE 등 다양한 HTTP 메서드와 JSON Request Body를 설정하고 테스트할 수 있도록 개선합니다. 이를 통해 토큰 발급 API나 데이터 전송 API 등 다양한 외부 시스템과의 연동을 지원합니다.
|
||||
|
||||
## 핵심 기능
|
||||
1. **DB 스키마 확장**: `external_rest_api_connections` 테이블에 `default_method`, `default_body` 컬럼 추가
|
||||
2. **백엔드 로직 개선**:
|
||||
- 커넥션 생성/수정 시 메서드와 바디 정보 저장
|
||||
- 연결 테스트 시 설정된 메서드와 바디를 사용하여 요청 수행
|
||||
- SSL 인증서 검증 우회 옵션 적용 (내부망/테스트망 지원)
|
||||
3. **프론트엔드 UI 개선**:
|
||||
- 커넥션 설정 모달에 HTTP 메서드 선택(Select) 및 Body 입력(Textarea/JSON Editor) 필드 추가
|
||||
- 테스트 기능에서 Body 데이터 포함하여 요청 전송
|
||||
|
||||
## 테스트 계획
|
||||
### 1단계: 기본 기능 및 DB 마이그레이션
|
||||
- [x] DB 마이그레이션 스크립트 작성 및 실행
|
||||
- [x] 백엔드 타입 정의 수정 (`default_method`, `default_body` 추가)
|
||||
|
||||
### 2단계: 백엔드 로직 구현
|
||||
- [x] 커넥션 생성/수정 API 수정 (필드 추가)
|
||||
- [x] 커넥션 상세 조회 API 확인
|
||||
- [x] 연결 테스트 API 수정 (Method, Body 반영하여 요청 전송)
|
||||
|
||||
### 3단계: 프론트엔드 구현
|
||||
- [x] 커넥션 관리 리스트/모달 UI 수정
|
||||
- [x] 연결 테스트 UI 수정 및 기능 확인
|
||||
|
||||
## 에러 처리 계획
|
||||
- **JSON 파싱 에러**: Body 입력값이 유효한 JSON이 아닐 경우 에러 처리
|
||||
- **API 호출 에러**: 외부 API 호출 실패 시 상세 로그 기록 및 클라이언트에 에러 메시지 전달
|
||||
- **SSL 인증 에러**: `rejectUnauthorized: false` 옵션으로 처리 (기존 `RestApiConnector` 활용)
|
||||
|
||||
## 진행 상태
|
||||
- [완료] 모든 단계 구현 완료
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## 1. 개요
|
||||
|
||||
현재 **68개 이상**으로 파편화된 화면 관리 컴포넌트들을 **9개의 핵심 통합 컴포넌트(V2 Components)**로 재편합니다.
|
||||
현재 **68개 이상**으로 파편화된 화면 관리 컴포넌트들을 **9개의 핵심 통합 컴포넌트(Unified Components)**로 재편합니다.
|
||||
각 컴포넌트는 **속성(Config)** 설정을 통해 다양한 형태(View Mode)와 기능(Behavior)을 수행하도록 설계되어, 유지보수성과 확장성을 극대화합니다.
|
||||
|
||||
### 현재 컴포넌트 현황 (AS-IS)
|
||||
|
|
@ -24,11 +24,11 @@
|
|||
|
||||
| 통합 컴포넌트 (TO-BE) | 포함되는 기존 컴포넌트 (AS-IS) | 핵심 속성 (Configuration) |
|
||||
| :-------------------- | :------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **1. V2 Select** | Select, Radio, Checkbox, Boolean, Code, Entity, Combobox, Toggle | **`mode`**: "dropdown" / "radio" / "check" / "tag"<br>**`source`**: "static" / "code" / "db" / "api"<br>**`dependency`**: { parentField: "..." } |
|
||||
| **2. V2 Input** | Text, Number, Email, Tel, Password, Color, Search, Integer, Decimal | **`type`**: "text" / "number" / "password"<br>**`format`**: "email", "currency", "biz_no"<br>**`mask`**: "000-0000-0000" |
|
||||
| **3. V2 Date** | Date, Time, DateTime, DateRange, Month, Year | **`type`**: "date" / "time" / "datetime"<br>**`range`**: true/false |
|
||||
| **4. V2 Text** | Textarea, RichEditor, Markdown, HTML | **`mode`**: "simple" / "rich" / "code"<br>**`rows`**: number |
|
||||
| **5. V2 Media** | File, Image, Video, Audio, Attachment | **`type`**: "file" / "image"<br>**`multiple`**: true/false<br>**`preview`**: true/false |
|
||||
| **1. Unified Select** | Select, Radio, Checkbox, Boolean, Code, Entity, Combobox, Toggle | **`mode`**: "dropdown" / "radio" / "check" / "tag"<br>**`source`**: "static" / "code" / "db" / "api"<br>**`dependency`**: { parentField: "..." } |
|
||||
| **2. Unified Input** | Text, Number, Email, Tel, Password, Color, Search, Integer, Decimal | **`type`**: "text" / "number" / "password"<br>**`format`**: "email", "currency", "biz_no"<br>**`mask`**: "000-0000-0000" |
|
||||
| **3. Unified Date** | Date, Time, DateTime, DateRange, Month, Year | **`type`**: "date" / "time" / "datetime"<br>**`range`**: true/false |
|
||||
| **4. Unified Text** | Textarea, RichEditor, Markdown, HTML | **`mode`**: "simple" / "rich" / "code"<br>**`rows`**: number |
|
||||
| **5. Unified Media** | File, Image, Video, Audio, Attachment | **`type`**: "file" / "image"<br>**`multiple`**: true/false<br>**`preview`**: true/false |
|
||||
|
||||
### B. 구조/데이터 위젯 (Structure & Data Widgets) - 4종
|
||||
|
||||
|
|
@ -36,10 +36,10 @@
|
|||
|
||||
| 통합 컴포넌트 (TO-BE) | 포함되는 기존 컴포넌트 (AS-IS) | 핵심 속성 (Configuration) | 활용 예시 |
|
||||
| :-------------------- | :-------------------------------------------------- | :------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------- |
|
||||
| **6. V2 List** | **Table, Card List, Repeater, DataGrid, List View** | **`viewMode`**: "table" / "card" / "kanban"<br>**`editable`**: true/false | - `viewMode='table'`: 엑셀형 리스트<br>- `viewMode='card'`: **카드 디스플레이**<br>- `editable=true`: **반복 필드 그룹** |
|
||||
| **7. V2 Layout** | **Row, Col, Split Panel, Grid, Spacer** | **`type`**: "grid" / "split" / "flex"<br>**`columns`**: number | - `type='split'`: **화면 분할 패널**<br>- `type='grid'`: 격자 레이아웃 |
|
||||
| **8. V2 Group** | Tab, Accordion, FieldSet, Modal, Section | **`type`**: "tab" / "accordion" / "modal" | - 탭이나 아코디언으로 내용 그룹화 |
|
||||
| **9. V2 Biz** | **Rack Structure**, Calendar, Gantt | **`type`**: "rack" / "calendar" / "gantt" | - `type='rack'`: **랙 구조 설정**<br>- 특수 비즈니스 로직 플러그인 탑재 |
|
||||
| **6. Unified List** | **Table, Card List, Repeater, DataGrid, List View** | **`viewMode`**: "table" / "card" / "kanban"<br>**`editable`**: true/false | - `viewMode='table'`: 엑셀형 리스트<br>- `viewMode='card'`: **카드 디스플레이**<br>- `editable=true`: **반복 필드 그룹** |
|
||||
| **7. Unified Layout** | **Row, Col, Split Panel, Grid, Spacer** | **`type`**: "grid" / "split" / "flex"<br>**`columns`**: number | - `type='split'`: **화면 분할 패널**<br>- `type='grid'`: 격자 레이아웃 |
|
||||
| **8. Unified Group** | Tab, Accordion, FieldSet, Modal, Section | **`type`**: "tab" / "accordion" / "modal" | - 탭이나 아코디언으로 내용 그룹화 |
|
||||
| **9. Unified Biz** | **Rack Structure**, Calendar, Gantt | **`type`**: "rack" / "calendar" / "gantt" | - `type='rack'`: **랙 구조 설정**<br>- 특수 비즈니스 로직 플러그인 탑재 |
|
||||
|
||||
### C. Config Panel 통합 전략 (핵심)
|
||||
|
||||
|
|
@ -60,16 +60,16 @@
|
|||
### Case 1: "테이블을 카드 리스트로 변경"
|
||||
|
||||
- **AS-IS**: `DataTable` 컴포넌트를 삭제하고 `CardList` 컴포넌트를 새로 추가해야 함.
|
||||
- **TO-BE**: `V2List`의 속성창에서 **[View Mode]**를 `Table` → `Card`로 변경하면 즉시 반영.
|
||||
- **TO-BE**: `UnifiedList`의 속성창에서 **[View Mode]**를 `Table` → `Card`로 변경하면 즉시 반영.
|
||||
|
||||
### Case 2: "단일 선택을 라디오 버튼으로 변경"
|
||||
|
||||
- **AS-IS**: `SelectWidget`을 삭제하고 `RadioWidget` 추가.
|
||||
- **TO-BE**: `V2Select` 속성창에서 **[Display Mode]**를 `Dropdown` → `Radio`로 변경.
|
||||
- **TO-BE**: `UnifiedSelect` 속성창에서 **[Display Mode]**를 `Dropdown` → `Radio`로 변경.
|
||||
|
||||
### Case 3: "입력 폼에 반복 필드(Repeater) 추가"
|
||||
|
||||
- **TO-BE**: `V2List` 컴포넌트 배치 후 `editable: true`, `viewMode: "table"` 설정.
|
||||
- **TO-BE**: `UnifiedList` 컴포넌트 배치 후 `editable: true`, `viewMode: "table"` 설정.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
통합 작업 전 필수 분석 및 설계를 진행합니다.
|
||||
|
||||
- [ ] 기존 컴포넌트 사용 현황 분석 (화면별 위젯 사용 빈도 조사)
|
||||
- [ ] 데이터 마이그레이션 전략 설계 (`widgetType` → `V2Widget.type` 매핑 정의)
|
||||
- [ ] 데이터 마이그레이션 전략 설계 (`widgetType` → `UnifiedWidget.type` 매핑 정의)
|
||||
- [ ] `sys_input_type` 테이블 JSON Schema 설계
|
||||
- [ ] DynamicConfigPanel 프로토타입 설계
|
||||
|
||||
|
|
@ -88,9 +88,9 @@
|
|||
|
||||
가장 중복이 많고 효과가 즉각적인 입력 필드부터 통합합니다.
|
||||
|
||||
- [ ] **V2Input 구현**: Text, Number, Email, Tel, Password 통합
|
||||
- [ ] **V2Select 구현**: Select, Radio, Checkbox, Boolean 통합
|
||||
- [ ] **V2Date 구현**: Date, DateTime, Time 통합
|
||||
- [ ] **UnifiedInput 구현**: Text, Number, Email, Tel, Password 통합
|
||||
- [ ] **UnifiedSelect 구현**: Select, Radio, Checkbox, Boolean 통합
|
||||
- [ ] **UnifiedDate 구현**: Date, DateTime, Time 통합
|
||||
- [ ] 기존 위젯과 **병행 운영** (deprecated 마킹, 삭제하지 않음)
|
||||
|
||||
### Phase 2: Config Panel 통합 (2주)
|
||||
|
|
@ -105,15 +105,15 @@
|
|||
|
||||
프로젝트의 데이터를 보여주는 핵심 뷰를 통합합니다.
|
||||
|
||||
- [ ] **V2List 구현**: Table, Card, Repeater 통합 렌더러 개발
|
||||
- [ ] **V2Layout 구현**: Split Panel, Grid, Flex 통합
|
||||
- [ ] **V2Group 구현**: Tab, Accordion, Modal 통합
|
||||
- [ ] **UnifiedList 구현**: Table, Card, Repeater 통합 렌더러 개발
|
||||
- [ ] **UnifiedLayout 구현**: Split Panel, Grid, Flex 통합
|
||||
- [ ] **UnifiedGroup 구현**: Tab, Accordion, Modal 통합
|
||||
|
||||
### Phase 4: 안정화 및 마이그레이션 (2주)
|
||||
|
||||
신규 컴포넌트 안정화 후 점진적 전환을 진행합니다.
|
||||
|
||||
- [ ] 신규 화면은 V2 컴포넌트만 사용하도록 가이드
|
||||
- [ ] 신규 화면은 Unified 컴포넌트만 사용하도록 가이드
|
||||
- [ ] 기존 화면 데이터 마이그레이션 스크립트 개발
|
||||
- [ ] 마이그레이션 테스트 (스테이징 환경)
|
||||
- [ ] 문서화 및 개발 가이드 작성
|
||||
|
|
@ -122,7 +122,7 @@
|
|||
|
||||
충분한 안정화 기간 후 레거시 컴포넌트 정리를 검토합니다.
|
||||
|
||||
- [ ] 사용 현황 재분석 (V2 전환율 확인)
|
||||
- [ ] 사용 현황 재분석 (Unified 전환율 확인)
|
||||
- [ ] 미전환 화면 목록 정리
|
||||
- [ ] 레거시 컴포넌트 삭제 여부 결정 (별도 회의)
|
||||
|
||||
|
|
@ -132,27 +132,27 @@
|
|||
|
||||
### 5.1 위젯 타입 매핑 테이블
|
||||
|
||||
기존 `widgetType`을 신규 V2 컴포넌트로 매핑합니다.
|
||||
기존 `widgetType`을 신규 Unified 컴포넌트로 매핑합니다.
|
||||
|
||||
| 기존 widgetType | 신규 컴포넌트 | 속성 설정 |
|
||||
| :-------------- | :------------ | :------------------------------ |
|
||||
| `text` | V2Input | `type: "text"` |
|
||||
| `number` | V2Input | `type: "number"` |
|
||||
| `email` | V2Input | `type: "text", format: "email"` |
|
||||
| `tel` | V2Input | `type: "text", format: "tel"` |
|
||||
| `select` | V2Select | `mode: "dropdown"` |
|
||||
| `radio` | V2Select | `mode: "radio"` |
|
||||
| `checkbox` | V2Select | `mode: "check"` |
|
||||
| `date` | V2Date | `type: "date"` |
|
||||
| `datetime` | V2Date | `type: "datetime"` |
|
||||
| `textarea` | V2Text | `mode: "simple"` |
|
||||
| `file` | V2Media | `type: "file"` |
|
||||
| `image` | V2Media | `type: "image"` |
|
||||
| `text` | UnifiedInput | `type: "text"` |
|
||||
| `number` | UnifiedInput | `type: "number"` |
|
||||
| `email` | UnifiedInput | `type: "text", format: "email"` |
|
||||
| `tel` | UnifiedInput | `type: "text", format: "tel"` |
|
||||
| `select` | UnifiedSelect | `mode: "dropdown"` |
|
||||
| `radio` | UnifiedSelect | `mode: "radio"` |
|
||||
| `checkbox` | UnifiedSelect | `mode: "check"` |
|
||||
| `date` | UnifiedDate | `type: "date"` |
|
||||
| `datetime` | UnifiedDate | `type: "datetime"` |
|
||||
| `textarea` | UnifiedText | `mode: "simple"` |
|
||||
| `file` | UnifiedMedia | `type: "file"` |
|
||||
| `image` | UnifiedMedia | `type: "image"` |
|
||||
|
||||
### 5.2 마이그레이션 원칙
|
||||
|
||||
1. **비파괴적 전환**: 기존 데이터 구조 유지, 신규 필드 추가 방식
|
||||
2. **하위 호환성**: 기존 `widgetType` 필드는 유지, `v2Type` 필드 추가
|
||||
2. **하위 호환성**: 기존 `widgetType` 필드는 유지, `unifiedType` 필드 추가
|
||||
3. **점진적 전환**: 화면 수정 시점에 자동 또는 수동 전환
|
||||
|
||||
---
|
||||
|
|
@ -183,7 +183,7 @@
|
|||
|
||||
현재 `frontend/lib/registry/components/`에 등록된 모든 컴포넌트의 통합 가능 여부를 분석했습니다.
|
||||
|
||||
#### V2Input으로 통합 (4개)
|
||||
#### UnifiedInput으로 통합 (4개)
|
||||
|
||||
| 현재 컴포넌트 | 매핑 속성 | 비고 |
|
||||
| :------------- | :--------------- | :------------- |
|
||||
|
|
@ -192,7 +192,7 @@
|
|||
| slider-basic | `type: "slider"` | 속성 추가 필요 |
|
||||
| button-primary | `type: "button"` | 별도 검토 |
|
||||
|
||||
#### V2Select로 통합 (8개)
|
||||
#### UnifiedSelect로 통합 (8개)
|
||||
|
||||
| 현재 컴포넌트 | 매핑 속성 | 비고 |
|
||||
| :------------------------ | :----------------------------------- | :------------- |
|
||||
|
|
@ -205,19 +205,19 @@
|
|||
| mail-recipient-selector | `mode: "multi", type: "email"` | 복합 컴포넌트 |
|
||||
| location-swap-selector | `mode: "swap"` | 특수 UI |
|
||||
|
||||
#### V2Date로 통합 (1개)
|
||||
#### UnifiedDate로 통합 (1개)
|
||||
|
||||
| 현재 컴포넌트 | 매핑 속성 | 비고 |
|
||||
| :------------ | :------------- | :--- |
|
||||
| date-input | `type: "date"` | |
|
||||
|
||||
#### V2Text로 통합 (1개)
|
||||
#### UnifiedText로 통합 (1개)
|
||||
|
||||
| 현재 컴포넌트 | 매핑 속성 | 비고 |
|
||||
| :------------- | :--------------- | :--- |
|
||||
| textarea-basic | `mode: "simple"` | |
|
||||
|
||||
#### V2Media로 통합 (3개)
|
||||
#### UnifiedMedia로 통합 (3개)
|
||||
|
||||
| 현재 컴포넌트 | 매핑 속성 | 비고 |
|
||||
| :------------ | :------------------------------ | :--- |
|
||||
|
|
@ -225,7 +225,7 @@
|
|||
| image-widget | `type: "image"` | |
|
||||
| image-display | `type: "image", readonly: true` | |
|
||||
|
||||
#### V2List로 통합 (8개)
|
||||
#### UnifiedList로 통합 (8개)
|
||||
|
||||
| 현재 컴포넌트 | 매핑 속성 | 비고 |
|
||||
| :-------------------- | :------------------------------------ | :------------ |
|
||||
|
|
@ -238,7 +238,7 @@
|
|||
| table-search-widget | `viewMode: "table", searchable: true` | |
|
||||
| tax-invoice-list | `viewMode: "table", bizType: "tax"` | 특수 비즈니스 |
|
||||
|
||||
#### V2Layout으로 통합 (4개)
|
||||
#### UnifiedLayout으로 통합 (4개)
|
||||
|
||||
| 현재 컴포넌트 | 매핑 속성 | 비고 |
|
||||
| :------------------ | :-------------------------- | :------------- |
|
||||
|
|
@ -247,7 +247,7 @@
|
|||
| divider-line | `type: "divider"` | 속성 추가 필요 |
|
||||
| screen-split-panel | `type: "screen-embed"` | 화면 임베딩 |
|
||||
|
||||
#### V2Group으로 통합 (5개)
|
||||
#### UnifiedGroup으로 통합 (5개)
|
||||
|
||||
| 현재 컴포넌트 | 매핑 속성 | 비고 |
|
||||
| :------------------- | :--------------------- | :------------ |
|
||||
|
|
@ -257,7 +257,7 @@
|
|||
| section-card | `type: "card-section"` | |
|
||||
| universal-form-modal | `type: "form-modal"` | 복합 컴포넌트 |
|
||||
|
||||
#### V2Biz로 통합 (7개)
|
||||
#### UnifiedBiz로 통합 (7개)
|
||||
|
||||
| 현재 컴포넌트 | 매핑 속성 | 비고 |
|
||||
| :-------------------- | :------------------------ | :--------------- |
|
||||
|
|
@ -274,8 +274,8 @@
|
|||
| 현재 컴포넌트 | 문제점 | 제안 |
|
||||
| :-------------------------- | :------------------- | :------------------------------ |
|
||||
| conditional-container | 조건부 렌더링 로직 | 공통 속성으로 분리 |
|
||||
| selected-items-detail-input | 복합 (선택+상세입력) | V2List + V2Group 조합 |
|
||||
| text-display | 읽기 전용 텍스트 | V2Input (readonly: true) |
|
||||
| selected-items-detail-input | 복합 (선택+상세입력) | UnifiedList + UnifiedGroup 조합 |
|
||||
| text-display | 읽기 전용 텍스트 | UnifiedInput (readonly: true) |
|
||||
|
||||
### 8.2 매핑 분석 결과
|
||||
|
||||
|
|
@ -291,7 +291,7 @@
|
|||
|
||||
### 8.3 속성 확장 필요 사항
|
||||
|
||||
#### V2Input 속성 확장
|
||||
#### UnifiedInput 속성 확장
|
||||
|
||||
```typescript
|
||||
// 기존
|
||||
|
|
@ -301,7 +301,7 @@ type: "text" | "number" | "password";
|
|||
type: "text" | "number" | "password" | "slider" | "color" | "button";
|
||||
```
|
||||
|
||||
#### V2Select 속성 확장
|
||||
#### UnifiedSelect 속성 확장
|
||||
|
||||
```typescript
|
||||
// 기존
|
||||
|
|
@ -311,7 +311,7 @@ mode: "dropdown" | "radio" | "check" | "tag";
|
|||
mode: "dropdown" | "radio" | "check" | "tag" | "toggle" | "swap";
|
||||
```
|
||||
|
||||
#### V2Layout 속성 확장
|
||||
#### UnifiedLayout 속성 확장
|
||||
|
||||
```typescript
|
||||
// 기존
|
||||
|
|
@ -326,8 +326,8 @@ type: "grid" | "split" | "flex" | "divider" | "screen-embed";
|
|||
`conditional-container`의 기능을 모든 컴포넌트에서 사용 가능한 공통 속성으로 분리합니다.
|
||||
|
||||
```typescript
|
||||
// 모든 V2 컴포넌트에 적용 가능한 공통 속성
|
||||
interface BaseV2Props {
|
||||
// 모든 Unified 컴포넌트에 적용 가능한 공통 속성
|
||||
interface BaseUnifiedProps {
|
||||
// ... 기존 속성
|
||||
|
||||
/** 조건부 렌더링 설정 */
|
||||
|
|
@ -356,12 +356,12 @@ DB 테이블 `cascading_hierarchy_group`에서 4가지 계층 타입을 지원
|
|||
| **BOM** | 자재명세서 구조 | 부품 > 하위부품 |
|
||||
| **TREE** | 일반 트리 | 카테고리 |
|
||||
|
||||
### 9.2 통합 방안: V2Hierarchy 신설 (10번째 컴포넌트)
|
||||
### 9.2 통합 방안: UnifiedHierarchy 신설 (10번째 컴포넌트)
|
||||
|
||||
계층 구조는 일반 입력/표시 위젯과 성격이 다르므로 **별도 컴포넌트로 분리**합니다.
|
||||
|
||||
```typescript
|
||||
interface V2HierarchyProps {
|
||||
interface UnifiedHierarchyProps {
|
||||
/** 계층 유형 */
|
||||
type: "tree" | "org" | "bom" | "cascading";
|
||||
|
||||
|
|
@ -400,16 +400,16 @@ interface V2HierarchyProps {
|
|||
|
||||
| # | 컴포넌트 | 역할 | 커버 범위 |
|
||||
| :-: | :------------------- | :------------- | :----------------------------------- |
|
||||
| 1 | **V2Input** | 단일 값 입력 | text, number, slider, button 등 |
|
||||
| 2 | **V2Select** | 선택 입력 | dropdown, radio, checkbox, toggle 등 |
|
||||
| 3 | **V2Date** | 날짜/시간 입력 | date, datetime, time, range |
|
||||
| 4 | **V2Text** | 다중 행 텍스트 | textarea, rich editor, markdown |
|
||||
| 5 | **V2Media** | 파일/미디어 | file, image, video, audio |
|
||||
| 6 | **V2List** | 데이터 목록 | table, card, repeater, kanban |
|
||||
| 7 | **V2Layout** | 레이아웃 배치 | grid, split, flex, divider |
|
||||
| 8 | **V2Group** | 콘텐츠 그룹화 | tabs, accordion, section, modal |
|
||||
| 9 | **V2Biz** | 비즈니스 특화 | flow, rack, map, numbering 등 |
|
||||
| 10 | **V2Hierarchy** | 계층 구조 | tree, org, bom, cascading |
|
||||
| 1 | **UnifiedInput** | 단일 값 입력 | text, number, slider, button 등 |
|
||||
| 2 | **UnifiedSelect** | 선택 입력 | dropdown, radio, checkbox, toggle 등 |
|
||||
| 3 | **UnifiedDate** | 날짜/시간 입력 | date, datetime, time, range |
|
||||
| 4 | **UnifiedText** | 다중 행 텍스트 | textarea, rich editor, markdown |
|
||||
| 5 | **UnifiedMedia** | 파일/미디어 | file, image, video, audio |
|
||||
| 6 | **UnifiedList** | 데이터 목록 | table, card, repeater, kanban |
|
||||
| 7 | **UnifiedLayout** | 레이아웃 배치 | grid, split, flex, divider |
|
||||
| 8 | **UnifiedGroup** | 콘텐츠 그룹화 | tabs, accordion, section, modal |
|
||||
| 9 | **UnifiedBiz** | 비즈니스 특화 | flow, rack, map, numbering 등 |
|
||||
| 10 | **UnifiedHierarchy** | 계층 구조 | tree, org, bom, cascading |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -443,14 +443,14 @@ interface V2HierarchyProps {
|
|||
|
||||
### 11.3 속성 통합 설계
|
||||
|
||||
#### 2단계 연쇄 → V2Select 속성
|
||||
#### 2단계 연쇄 → UnifiedSelect 속성
|
||||
|
||||
```typescript
|
||||
// AS-IS: 별도 관리 메뉴에서 정의 후 참조
|
||||
<SelectWidget cascadingRelation="WAREHOUSE_LOCATION" />
|
||||
|
||||
// TO-BE: 컴포넌트 속성에서 직접 정의
|
||||
<V2Select
|
||||
<UnifiedSelect
|
||||
source="db"
|
||||
table="warehouse_location"
|
||||
valueColumn="location_code"
|
||||
|
|
@ -470,7 +470,7 @@ interface V2HierarchyProps {
|
|||
// cascading_condition 테이블에 저장
|
||||
|
||||
// TO-BE: 모든 컴포넌트에 공통 속성으로 적용
|
||||
<V2Input
|
||||
<UnifiedInput
|
||||
conditional={{
|
||||
enabled: true,
|
||||
field: "order_type", // 참조할 필드
|
||||
|
|
@ -487,7 +487,7 @@ interface V2HierarchyProps {
|
|||
// AS-IS: cascading_auto_fill_group 테이블에 정의
|
||||
|
||||
// TO-BE: 컴포넌트 속성에서 직접 정의
|
||||
<V2Input
|
||||
<UnifiedInput
|
||||
autoFill={{
|
||||
enabled: true,
|
||||
sourceTable: "company_mng", // 조회할 테이블
|
||||
|
|
@ -504,7 +504,7 @@ interface V2HierarchyProps {
|
|||
// AS-IS: cascading_mutual_exclusion 테이블에 정의
|
||||
|
||||
// TO-BE: 컴포넌트 속성에서 직접 정의
|
||||
<V2Select
|
||||
<UnifiedSelect
|
||||
mutualExclusion={{
|
||||
enabled: true,
|
||||
targetField: "sub_category", // 상호 배제 대상 필드
|
||||
|
|
@ -518,7 +518,7 @@ interface V2HierarchyProps {
|
|||
| 현재 메뉴 | TO-BE | 비고 |
|
||||
| :-------------------------- | :----------------------- | :-------------------- |
|
||||
| **연쇄 드롭다운 통합 관리** | **삭제** | 6개 탭 전체 제거 |
|
||||
| ├─ 2단계 연쇄관계 | V2Select 속성 | inline 정의 |
|
||||
| ├─ 2단계 연쇄관계 | UnifiedSelect 속성 | inline 정의 |
|
||||
| ├─ 다단계 계층 | **테이블관리로 이동** | 복잡한 구조 유지 필요 |
|
||||
| ├─ 조건부 필터 | 공통 conditional 속성 | 모든 컴포넌트에 적용 |
|
||||
| ├─ 자동 입력 | autoFill 속성 | 컴포넌트별 정의 |
|
||||
|
|
@ -557,21 +557,21 @@ interface V2HierarchyProps {
|
|||
|
||||
| # | 컴포넌트 | 역할 |
|
||||
| :-: | :------------------- | :--------------------------------------- |
|
||||
| 1 | **V2Input** | 단일 값 입력 (text, number, slider 등) |
|
||||
| 2 | **V2Select** | 선택 입력 (dropdown, radio, checkbox 등) |
|
||||
| 3 | **V2Date** | 날짜/시간 입력 |
|
||||
| 4 | **V2Text** | 다중 행 텍스트 (textarea, rich editor) |
|
||||
| 5 | **V2Media** | 파일/미디어 (file, image) |
|
||||
| 6 | **V2List** | 데이터 목록 (table, card, repeater) |
|
||||
| 7 | **V2Layout** | 레이아웃 배치 (grid, split, flex) |
|
||||
| 8 | **V2Group** | 콘텐츠 그룹화 (tabs, accordion, section) |
|
||||
| 9 | **V2Biz** | 비즈니스 특화 (flow, rack, map 등) |
|
||||
| 10 | **V2Hierarchy** | 계층 구조 (tree, org, bom, cascading) |
|
||||
| 1 | **UnifiedInput** | 단일 값 입력 (text, number, slider 등) |
|
||||
| 2 | **UnifiedSelect** | 선택 입력 (dropdown, radio, checkbox 등) |
|
||||
| 3 | **UnifiedDate** | 날짜/시간 입력 |
|
||||
| 4 | **UnifiedText** | 다중 행 텍스트 (textarea, rich editor) |
|
||||
| 5 | **UnifiedMedia** | 파일/미디어 (file, image) |
|
||||
| 6 | **UnifiedList** | 데이터 목록 (table, card, repeater) |
|
||||
| 7 | **UnifiedLayout** | 레이아웃 배치 (grid, split, flex) |
|
||||
| 8 | **UnifiedGroup** | 콘텐츠 그룹화 (tabs, accordion, section) |
|
||||
| 9 | **UnifiedBiz** | 비즈니스 특화 (flow, rack, map 등) |
|
||||
| 10 | **UnifiedHierarchy** | 계층 구조 (tree, org, bom, cascading) |
|
||||
|
||||
### 12.2 공통 속성 (모든 컴포넌트에 적용)
|
||||
|
||||
```typescript
|
||||
interface BaseV2Props {
|
||||
interface BaseUnifiedProps {
|
||||
// 기본 속성
|
||||
id: string;
|
||||
label?: string;
|
||||
|
|
@ -614,10 +614,10 @@ interface BaseV2Props {
|
|||
}
|
||||
```
|
||||
|
||||
### 12.3 V2Select 전용 속성
|
||||
### 12.3 UnifiedSelect 전용 속성
|
||||
|
||||
```typescript
|
||||
interface V2SelectProps extends BaseV2Props {
|
||||
interface UnifiedSelectProps extends BaseUnifiedProps {
|
||||
// 표시 모드
|
||||
mode: "dropdown" | "radio" | "check" | "tag" | "toggle" | "swap";
|
||||
|
||||
|
|
@ -660,11 +660,11 @@ interface V2SelectProps extends BaseV2Props {
|
|||
| AS-IS | TO-BE |
|
||||
| :---------------------------- | :----------------------------------- |
|
||||
| 연쇄 드롭다운 통합 관리 (6탭) | **삭제** |
|
||||
| - 2단계 연쇄관계 | → V2Select.cascading 속성 |
|
||||
| - 2단계 연쇄관계 | → UnifiedSelect.cascading 속성 |
|
||||
| - 다단계 계층 | → 테이블관리 > 계층 구조 설정 |
|
||||
| - 조건부 필터 | → 공통 conditional 속성 |
|
||||
| - 자동 입력 | → 공통 autoFill 속성 |
|
||||
| - 상호 배제 | → V2Select.mutualExclusion 속성 |
|
||||
| - 상호 배제 | → UnifiedSelect.mutualExclusion 속성 |
|
||||
| - 카테고리 값 연쇄 | → 카테고리 관리와 통합 |
|
||||
|
||||
---
|
||||
1041
POPUPDATE.md
696
POPUPDATE_2.md
|
|
@ -1,696 +0,0 @@
|
|||
# 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`
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
# 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. **네트워크**: 방화벽, 포트 충돌
|
||||
|
||||
---
|
||||
|
||||
**🎉 즐거운 개발 되세요!**
|
||||
523
README.md
|
|
@ -1,175 +1,472 @@
|
|||
# WACE 솔루션 (ERP/PLM)
|
||||
# WACE 솔루션 (PLM)
|
||||
|
||||
## 프로젝트 개요
|
||||
|
||||
본 프로젝트는 WACE ERP/PLM(Product Lifecycle Management) 솔루션입니다.
|
||||
Node.js + Next.js 기반 풀스택 웹 애플리케이션으로, 멀티테넌시를 지원합니다.
|
||||
본 프로젝트는 제품 수명 주기 관리(PLM - Product Lifecycle Management) 솔루션입니다.
|
||||
**기존 JSP 기반 프론트엔드를 Next.js 14로 완전 전환**하여 현대적이고 사용자 친화적인 웹 애플리케이션을 제공합니다.
|
||||
|
||||
## 주요 특징
|
||||
## 🚀 주요 특징
|
||||
|
||||
- **모던 프론트엔드**: Next.js (App Router) + TypeScript + shadcn/ui
|
||||
- **Node.js 백엔드**: Express + TypeScript + PostgreSQL
|
||||
- **모던 프론트엔드**: Next.js 14 + TypeScript + shadcn/ui
|
||||
- **반응형 디자인**: 데스크톱, 태블릿, 모바일 모든 기기 지원
|
||||
- **멀티테넌시**: 회사별 데이터 격리 (company_code 기반)
|
||||
- **안정적인 백엔드**: 검증된 Java Spring + MyBatis 기반 API
|
||||
- **Docker 기반 배포**: 개발/운영 환경 일관성 보장
|
||||
- **타입 안전성**: TypeScript로 런타임 에러 방지
|
||||
|
||||
## 기술 스택
|
||||
## 🛠️ 기술 스택
|
||||
|
||||
### Frontend
|
||||
### Frontend (Next.js 14)
|
||||
|
||||
- **프레임워크**: Next.js (App Router, Turbopack)
|
||||
- **프레임워크**: Next.js 14 (App Router)
|
||||
- **언어**: TypeScript
|
||||
- **UI 라이브러리**: shadcn/ui + Radix UI
|
||||
- **스타일링**: Tailwind CSS
|
||||
- **상태 관리**: TanStack Query + React Context
|
||||
- **폼 처리**: React Hook Form + Zod
|
||||
- **상태 관리**: React Context + Hooks
|
||||
- **아이콘**: Lucide React
|
||||
|
||||
### Backend
|
||||
### Backend (기존 유지)
|
||||
|
||||
- **런타임**: Node.js 20+
|
||||
- **프레임워크**: Express 4
|
||||
- **언어**: TypeScript
|
||||
- **데이터베이스**: PostgreSQL (pg 드라이버)
|
||||
- **인증**: JWT (jsonwebtoken) + bcryptjs
|
||||
- **로깅**: Winston
|
||||
- **언어**: Java 7
|
||||
- **프레임워크**: Spring Framework 3.2.4
|
||||
- **ORM**: MyBatis 3.2.3
|
||||
- **데이터베이스**: PostgreSQL
|
||||
- **WAS**: Apache Tomcat 7.0
|
||||
|
||||
### 개발 도구
|
||||
|
||||
- **컨테이너화**: Docker + Docker Compose
|
||||
- **코드 품질**: ESLint + Prettier
|
||||
- **테스트**: Jest + Supertest
|
||||
- **백엔드 핫리로드**: nodemon
|
||||
- **CI/CD**: Jenkins
|
||||
- **코드 품질**: ESLint + TypeScript
|
||||
- **패키지 관리**: npm
|
||||
|
||||
## 프로젝트 구조
|
||||
## 📁 프로젝트 구조
|
||||
|
||||
```
|
||||
ERP-node/
|
||||
├── backend-node/ # Express + TypeScript 백엔드
|
||||
│ ├── src/
|
||||
│ │ ├── app.ts # 엔트리포인트
|
||||
│ │ ├── controllers/ # API 컨트롤러
|
||||
│ │ ├── services/ # 비즈니스 로직
|
||||
│ │ ├── middleware/ # 인증, 에러처리 미들웨어
|
||||
│ │ ├── routes/ # 라우터
|
||||
│ │ └── config/ # DB 연결 등 설정
|
||||
│ └── package.json
|
||||
new_ph/
|
||||
├── frontend/ # Next.js 프론트엔드
|
||||
│ ├── app/ # App Router 페이지
|
||||
│ ├── components/ # React 컴포넌트
|
||||
│ ├── app/ # Next.js App Router
|
||||
│ │ ├── (auth)/ # 인증 관련 페이지
|
||||
│ │ │ └── login/ # 로그인 페이지
|
||||
│ │ ├── dashboard/ # 대시보드
|
||||
│ │ └── layout.tsx # 루트 레이아웃
|
||||
│ ├── components/ # 재사용 컴포넌트
|
||||
│ │ ├── ui/ # shadcn/ui 기본 컴포넌트
|
||||
│ │ ├── admin/ # 관리자 컴포넌트
|
||||
│ │ ├── screen/ # 화면 디자이너
|
||||
│ │ └── v2/ # V2 컴포넌트
|
||||
│ ├── lib/ # 유틸리티, API 클라이언트
|
||||
│ ├── hooks/ # Custom React Hooks
|
||||
│ └── package.json
|
||||
├── db/ # 데이터베이스
|
||||
│ └── migrations/ # 순차 마이그레이션 SQL
|
||||
├── docker/ # Docker 설정 (dev/prod/deploy)
|
||||
├── scripts/ # 개발/배포 스크립트
|
||||
├── docs/ # 프로젝트 문서
|
||||
├── Dockerfile # 프로덕션 멀티스테이지 빌드
|
||||
├── Jenkinsfile # CI/CD 파이프라인
|
||||
└── .cursorrules # AI 개발 가이드
|
||||
│ │ └── layout/ # 레이아웃 컴포넌트
|
||||
│ ├── lib/ # 유틸리티 함수
|
||||
│ └── types/ # TypeScript 타입 정의
|
||||
├── src/ # Java 백엔드 소스
|
||||
│ └── com/pms/ # 패키지 구조
|
||||
├── WebContent/ # 레거시 JSP (사용 중단)
|
||||
├── db/ # 데이터베이스 스크립트
|
||||
└── docker-compose.win.yml # Windows 환경 설정
|
||||
```
|
||||
|
||||
## 빠른 시작
|
||||
## 🚀 빠른 시작
|
||||
|
||||
### 1. 필수 요구사항
|
||||
|
||||
- **Node.js**: 20.10+
|
||||
- **PostgreSQL**: 데이터베이스 서버
|
||||
- **npm**: 10.0+
|
||||
- **Docker Desktop** (Windows/Mac) 또는 **Docker + Docker Compose** (Linux)
|
||||
- **Git** (소스 코드 관리)
|
||||
|
||||
### 2. 개발 환경 실행
|
||||
### 2. Windows 환경에서 실행
|
||||
|
||||
#### 자동 실행 (권장)
|
||||
|
||||
```bash
|
||||
# 백엔드 (nodemon으로 자동 재시작)
|
||||
cd backend-node && npm install && npm run dev
|
||||
# 프로젝트 시작
|
||||
./run-windows.bat
|
||||
|
||||
# 프론트엔드 (Turbopack)
|
||||
cd frontend && npm install && npm run dev
|
||||
# 서비스 상태 확인
|
||||
./status-windows.bat
|
||||
|
||||
# 서비스 중지
|
||||
./stop-windows.bat
|
||||
```
|
||||
|
||||
### 3. Docker 환경 실행
|
||||
#### 수동 실행
|
||||
|
||||
```bash
|
||||
# 백엔드 + 프론트엔드 (개발)
|
||||
docker-compose -f docker-compose.backend.win.yml up -d
|
||||
docker-compose -f docker-compose.frontend.win.yml up -d
|
||||
# Docker 컨테이너 실행
|
||||
docker-compose -f docker-compose.win.yml up --build -d
|
||||
|
||||
# 프로덕션 배포
|
||||
docker-compose -f docker/deploy/docker-compose.yml up -d
|
||||
# 로그 확인
|
||||
docker-compose -f docker-compose.win.yml logs -f
|
||||
```
|
||||
|
||||
### 4. 서비스 접속
|
||||
### 3. 서비스 접속
|
||||
|
||||
| 서비스 | URL | 설명 |
|
||||
| -------------- | --------------------- | ------------------------------ |
|
||||
| **프론트엔드** | http://localhost:9771 | Next.js 사용자 인터페이스 |
|
||||
| **백엔드 API** | http://localhost:8080 | Express REST API |
|
||||
| **프론트엔드** | http://localhost:9771 | Next.js 기반 사용자 인터페이스 |
|
||||
| **백엔드 API** | http://localhost:9090 | Spring 기반 REST API |
|
||||
|
||||
## 주요 기능
|
||||
> **주의**: 기존 JSP 화면(`localhost:9090`)은 더 이상 사용하지 않습니다.
|
||||
> 모든 사용자는 **Next.js 프론트엔드(`localhost:9771`)**를 사용해주세요.
|
||||
|
||||
### 1. 사용자 및 권한 관리
|
||||
- 사용자 계정 관리 (CRUD)
|
||||
- 역할 기반 접근 제어 (RBAC)
|
||||
- 부서/조직 관리
|
||||
- 멀티테넌시 (회사별 데이터 격리)
|
||||
## 🎨 UI/UX 디자인 시스템
|
||||
|
||||
### 2. 메뉴 및 화면 관리
|
||||
- 동적 메뉴 구성
|
||||
- 화면 디자이너 (드래그앤드롭)
|
||||
- V2 컴포넌트 시스템
|
||||
### shadcn/ui 컴포넌트 라이브러리
|
||||
|
||||
### 3. 플로우(워크플로우) 관리
|
||||
- 비즈니스 프로세스 정의
|
||||
- 데이터 흐름 관리
|
||||
- 감사 로그
|
||||
- **일관된 디자인**: 전체 애플리케이션에서 통일된 UI 컴포넌트
|
||||
- **접근성**: WCAG 가이드라인 준수
|
||||
- **커스터마이징**: Tailwind CSS로 쉬운 스타일 변경
|
||||
- **다크모드**: 자동 테마 전환 지원
|
||||
|
||||
### 4. 제품/BOM 관리
|
||||
- BOM 구성 및 버전 관리
|
||||
- 제품 정보 관리
|
||||
### 공통 스타일 가이드
|
||||
|
||||
### 5. 기타
|
||||
- 파일/문서 관리
|
||||
- 메일 연동
|
||||
- 외부 DB 연결
|
||||
- 번호 채번 규칙
|
||||
```typescript
|
||||
// 색상 팔레트
|
||||
const colors = {
|
||||
primary: "hsl(222.2 47.4% 11.2%)", // 네이비 블루
|
||||
secondary: "hsl(210 40% 96%)", // 연한 그레이
|
||||
accent: "hsl(210 40% 98%)", // 거의 화이트
|
||||
destructive: "hsl(0 62.8% 30.6%)", // 레드
|
||||
muted: "hsl(210 40% 96%)", // 음소거된 그레이
|
||||
};
|
||||
|
||||
## 환경 변수
|
||||
|
||||
```bash
|
||||
# backend-node/.env
|
||||
DATABASE_URL=postgresql://postgres:password@localhost:5432/dbname
|
||||
JWT_SECRET=your-jwt-secret
|
||||
JWT_EXPIRES_IN=24h
|
||||
PORT=8080
|
||||
CORS_ORIGIN=http://localhost:9771
|
||||
|
||||
# frontend/.env.local
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8080/api
|
||||
// 타이포그래피
|
||||
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
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 배포
|
||||
## 🔧 개발 가이드
|
||||
|
||||
### 프로덕션 빌드
|
||||
### 컴포넌트 개발 원칙
|
||||
|
||||
```bash
|
||||
# 멀티스테이지 Docker 빌드 (백엔드 + 프론트엔드)
|
||||
docker build -t wace-solution .
|
||||
#### 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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### CI/CD
|
||||
#### 2. 폼 컴포넌트
|
||||
|
||||
Jenkins 파이프라인 (`Jenkinsfile`)으로 자동 빌드 및 배포가 설정되어 있습니다.
|
||||
```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
|
||||
# 프론트엔드 개발 서버
|
||||
cd frontend && npm run dev
|
||||
|
||||
# 백엔드 (Docker)
|
||||
docker-compose -f docker-compose.win.yml up -d plm-app
|
||||
```
|
||||
|
||||
### 운영 환경
|
||||
|
||||
```bash
|
||||
# 전체 서비스 배포
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# 무중단 배포 (blue-green)
|
||||
./deploy-production.sh
|
||||
```
|
||||
|
||||
### 환경 변수 설정
|
||||
|
||||
```bash
|
||||
# .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**: 엄격한 타입 정의 사용
|
||||
- **ESLint + Prettier**: 일관된 코드 스타일
|
||||
- **shadcn/ui**: UI 컴포넌트 표준
|
||||
- **API 클라이언트**: `frontend/lib/api/` 전용 클라이언트 사용 (fetch 직접 사용 금지)
|
||||
- **멀티테넌시**: 모든 쿼리에 company_code 필터링 필수
|
||||
- **ESLint**: 코드 품질 유지.
|
||||
- **Prettier**: 일관된 코드 포맷팅
|
||||
- **커밋 메시지**: Conventional Commits 규칙 준수
|
||||
|
||||
### 브랜치 전략
|
||||
|
||||
```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에서는 완전히 새로운 사용자 경험을 제공합니다!**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
<Context docBase="ilshin" path="/" reloadable="true" source="org.eclipse.jst.jee.server:ilshin">
|
||||
<Resource auth="Container" driverClassName="org.postgresql.Driver" maxActive="100" maxIdle="10" maxWait="-1" name="plm" password="admin0909!!" type="javax.sql.DataSource" url="jdbc:postgresql://211.224.136.4:5432/ilshin" username="postgres"/>
|
||||
</Context>
|
||||
46
STATUS.md
|
|
@ -1,46 +0,0 @@
|
|||
# 프로젝트 상태 추적
|
||||
|
||||
> **최종 업데이트**: 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`으로 마이그레이션 안 됨 | 낮음 | 이전에 작동 안 했으므로 실질 영향 없음 |
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
Manifest-Version: 1.0
|
||||
Class-Path:
|
||||
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
<!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>
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
@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}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
@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}
|
||||
|
|
@ -0,0 +1,462 @@
|
|||
@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}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
@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}
|
||||
|
After Width: | Height: | Size: 115 B |
|
After Width: | Height: | Size: 526 B |
|
After Width: | Height: | Size: 331 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 159 B |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 103 B |
|
After Width: | Height: | Size: 43 B |
|
After Width: | Height: | Size: 56 B |
|
After Width: | Height: | Size: 941 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 104 B |
|
After Width: | Height: | Size: 139 B |
|
After Width: | Height: | Size: 155 B |
|
After Width: | Height: | Size: 270 B |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
|
@ -0,0 +1,134 @@
|
|||
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";
|
||||
}
|
||||
})();
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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
|
||||
};
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
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
|
||||
여기입니다! :)
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
==============================================================================================
|
||||
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 모드를 제공하여 텍스트만으로 본문의 내용을 작성할 수 있도록 편집 모드를 추가 제공한다.
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<!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>
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* @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;
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,684 @@
|
|||
//변수 선언 및 초기화
|
||||
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;
|
||||
};
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<!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>
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
<%--------------------------------------------------------------------------------
|
||||
* 화면명 : 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);
|
||||
// ./로컬경로에 파일 저장하기 ============================================
|
||||
%>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?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);
|
||||
?>
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
<%--------------------------------------------------------------------------------
|
||||
* 화면명 : 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);
|
||||
|
||||
// ./로컬경로에 파일 저장하기 ============================================
|
||||
%>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?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;
|
||||
}
|
||||
?>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 718 B |
|
After Width: | Height: | Size: 553 B |
|
After Width: | Height: | Size: 553 B |
|
After Width: | Height: | Size: 157 B |
|
After Width: | Height: | Size: 506 B |
|
|
@ -0,0 +1,390 @@
|
|||
/**
|
||||
* 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);
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
<!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>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<!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>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<!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>
|
||||