투명색 설정 가능하게 구현
This commit is contained in:
parent
1506389757
commit
ba817980f0
|
|
@ -0,0 +1,238 @@
|
|||
# 마이그레이션 063-064: 재고 관리 테이블 생성
|
||||
|
||||
## 목적
|
||||
|
||||
재고 현황 관리 및 입출고 이력 추적을 위한 테이블 생성
|
||||
|
||||
**테이블 타입관리 UI와 동일한 방식으로 생성됩니다.**
|
||||
|
||||
### 생성되는 테이블
|
||||
|
||||
| 테이블명 | 설명 | 용도 |
|
||||
|----------|------|------|
|
||||
| `inventory_stock` | 재고 현황 | 품목+로트별 현재 재고 상태 |
|
||||
| `inventory_history` | 재고 이력 | 입출고 트랜잭션 기록 |
|
||||
|
||||
---
|
||||
|
||||
## 테이블 타입관리 UI 방식 특징
|
||||
|
||||
1. **기본 컬럼 자동 포함**: `id`, `created_date`, `updated_date`, `writer`, `company_code`
|
||||
2. **데이터 타입 통일**: 날짜는 `TIMESTAMP`, 나머지는 `VARCHAR(500)`
|
||||
3. **메타데이터 등록**:
|
||||
- `table_labels`: 테이블 정보
|
||||
- `column_labels`: 컬럼 정보 (라벨, input_type, detail_settings)
|
||||
- `table_type_columns`: 회사별 컬럼 타입 정보
|
||||
|
||||
---
|
||||
|
||||
## 테이블 구조
|
||||
|
||||
### 1. inventory_stock (재고 현황)
|
||||
|
||||
| 컬럼명 | 타입 | input_type | 설명 |
|
||||
|--------|------|------------|------|
|
||||
| id | VARCHAR(500) | text | PK (자동생성) |
|
||||
| created_date | TIMESTAMP | date | 생성일시 |
|
||||
| updated_date | TIMESTAMP | date | 수정일시 |
|
||||
| writer | VARCHAR(500) | text | 작성자 |
|
||||
| company_code | VARCHAR(500) | text | 회사코드 |
|
||||
| item_code | VARCHAR(500) | text | 품목코드 |
|
||||
| lot_number | VARCHAR(500) | text | 로트번호 |
|
||||
| warehouse_id | VARCHAR(500) | entity | 창고 (FK → warehouse_info) |
|
||||
| location_code | VARCHAR(500) | text | 위치코드 |
|
||||
| current_qty | VARCHAR(500) | number | 현재고량 |
|
||||
| safety_qty | VARCHAR(500) | number | 안전재고 |
|
||||
| last_in_date | TIMESTAMP | date | 최종입고일 |
|
||||
| last_out_date | TIMESTAMP | date | 최종출고일 |
|
||||
|
||||
### 2. inventory_history (재고 이력)
|
||||
|
||||
| 컬럼명 | 타입 | input_type | 설명 |
|
||||
|--------|------|------------|------|
|
||||
| id | VARCHAR(500) | text | PK (자동생성) |
|
||||
| created_date | TIMESTAMP | date | 생성일시 |
|
||||
| updated_date | TIMESTAMP | date | 수정일시 |
|
||||
| writer | VARCHAR(500) | text | 작성자 |
|
||||
| company_code | VARCHAR(500) | text | 회사코드 |
|
||||
| stock_id | VARCHAR(500) | text | 재고ID (FK) |
|
||||
| item_code | VARCHAR(500) | text | 품목코드 |
|
||||
| lot_number | VARCHAR(500) | text | 로트번호 |
|
||||
| transaction_type | VARCHAR(500) | code | 구분 (IN/OUT) |
|
||||
| transaction_date | TIMESTAMP | date | 일자 |
|
||||
| quantity | VARCHAR(500) | number | 수량 |
|
||||
| balance_qty | VARCHAR(500) | number | 재고량 |
|
||||
| manager_id | VARCHAR(500) | text | 담당자ID |
|
||||
| manager_name | VARCHAR(500) | text | 담당자명 |
|
||||
| remark | VARCHAR(500) | text | 비고 |
|
||||
| reference_type | VARCHAR(500) | text | 참조문서유형 |
|
||||
| reference_id | VARCHAR(500) | text | 참조문서ID |
|
||||
| reference_number | VARCHAR(500) | text | 참조문서번호 |
|
||||
|
||||
---
|
||||
|
||||
## 실행 방법
|
||||
|
||||
### Docker 환경 (권장)
|
||||
|
||||
```bash
|
||||
# 재고 현황 테이블
|
||||
docker exec -i erp-node-db-1 psql -U postgres -d ilshin < db/migrations/063_create_inventory_stock.sql
|
||||
|
||||
# 재고 이력 테이블
|
||||
docker exec -i erp-node-db-1 psql -U postgres -d ilshin < db/migrations/064_create_inventory_history.sql
|
||||
```
|
||||
|
||||
### 로컬 PostgreSQL
|
||||
|
||||
```bash
|
||||
psql -U postgres -d ilshin -f db/migrations/063_create_inventory_stock.sql
|
||||
psql -U postgres -d ilshin -f db/migrations/064_create_inventory_history.sql
|
||||
```
|
||||
|
||||
### pgAdmin / DBeaver
|
||||
|
||||
1. 각 SQL 파일 열기
|
||||
2. 전체 내용 복사
|
||||
3. SQL 쿼리 창에 붙여넣기
|
||||
4. 실행 (F5 또는 Execute)
|
||||
|
||||
---
|
||||
|
||||
## 검증 방법
|
||||
|
||||
### 1. 테이블 생성 확인
|
||||
|
||||
```sql
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_name IN ('inventory_stock', 'inventory_history');
|
||||
```
|
||||
|
||||
### 2. 메타데이터 등록 확인
|
||||
|
||||
```sql
|
||||
-- table_labels
|
||||
SELECT * FROM table_labels WHERE table_name IN ('inventory_stock', 'inventory_history');
|
||||
|
||||
-- column_labels
|
||||
SELECT table_name, column_name, column_label, input_type, display_order
|
||||
FROM column_labels
|
||||
WHERE table_name IN ('inventory_stock', 'inventory_history')
|
||||
ORDER BY table_name, display_order;
|
||||
|
||||
-- table_type_columns
|
||||
SELECT table_name, column_name, company_code, input_type, display_order
|
||||
FROM table_type_columns
|
||||
WHERE table_name IN ('inventory_stock', 'inventory_history')
|
||||
ORDER BY table_name, display_order;
|
||||
```
|
||||
|
||||
### 3. 샘플 데이터 확인
|
||||
|
||||
```sql
|
||||
-- 재고 현황
|
||||
SELECT * FROM inventory_stock WHERE company_code = 'WACE';
|
||||
|
||||
-- 재고 이력
|
||||
SELECT * FROM inventory_history WHERE company_code = 'WACE' ORDER BY transaction_date;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 화면에서 사용할 조회 쿼리 예시
|
||||
|
||||
### 재고 현황 그리드 (좌측)
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
s.item_code,
|
||||
i.item_name,
|
||||
i.size as specification,
|
||||
i.unit,
|
||||
s.lot_number,
|
||||
w.warehouse_name,
|
||||
s.location_code,
|
||||
s.current_qty::numeric as current_qty,
|
||||
s.safety_qty::numeric as safety_qty,
|
||||
CASE
|
||||
WHEN s.current_qty::numeric < s.safety_qty::numeric THEN '부족'
|
||||
WHEN s.current_qty::numeric > s.safety_qty::numeric * 2 THEN '과다'
|
||||
ELSE '정상'
|
||||
END AS stock_status,
|
||||
s.last_in_date,
|
||||
s.last_out_date
|
||||
FROM inventory_stock s
|
||||
LEFT JOIN item_info i ON s.item_code = i.item_number AND s.company_code = i.company_code
|
||||
LEFT JOIN warehouse_info w ON s.warehouse_id = w.id
|
||||
WHERE s.company_code = 'WACE'
|
||||
ORDER BY s.item_code, s.lot_number;
|
||||
```
|
||||
|
||||
### 재고 이력 패널 (우측)
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
h.transaction_type,
|
||||
h.transaction_date,
|
||||
h.quantity,
|
||||
h.balance_qty,
|
||||
h.manager_name,
|
||||
h.remark
|
||||
FROM inventory_history h
|
||||
WHERE h.item_code = 'A001'
|
||||
AND h.lot_number = 'LOT-2024-001'
|
||||
AND h.company_code = 'WACE'
|
||||
ORDER BY h.transaction_date DESC, h.created_date DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 데이터 흐름
|
||||
|
||||
```
|
||||
[입고 발생]
|
||||
│
|
||||
├─→ inventory_history에 INSERT (+수량, 잔량)
|
||||
│
|
||||
└─→ inventory_stock에 UPDATE (current_qty 증가, last_in_date 갱신)
|
||||
|
||||
[출고 발생]
|
||||
│
|
||||
├─→ inventory_history에 INSERT (-수량, 잔량)
|
||||
│
|
||||
└─→ inventory_stock에 UPDATE (current_qty 감소, last_out_date 갱신)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 롤백 방법 (문제 발생 시)
|
||||
|
||||
```sql
|
||||
-- 테이블 삭제
|
||||
DROP TABLE IF EXISTS inventory_history;
|
||||
DROP TABLE IF EXISTS inventory_stock;
|
||||
|
||||
-- 메타데이터 삭제
|
||||
DELETE FROM column_labels WHERE table_name IN ('inventory_stock', 'inventory_history');
|
||||
DELETE FROM table_labels WHERE table_name IN ('inventory_stock', 'inventory_history');
|
||||
DELETE FROM table_type_columns WHERE table_name IN ('inventory_stock', 'inventory_history');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 관련 테이블 (마스터 데이터)
|
||||
|
||||
| 테이블 | 역할 | 연결 컬럼 |
|
||||
|--------|------|-----------|
|
||||
| item_info | 품목 마스터 | item_number |
|
||||
| warehouse_info | 창고 마스터 | id |
|
||||
| warehouse_location | 위치 마스터 | location_code |
|
||||
|
||||
---
|
||||
|
||||
**작성일**: 2025-12-09
|
||||
**영향 범위**: 재고 관리 시스템
|
||||
**생성 방식**: 테이블 타입관리 UI와 동일
|
||||
|
||||
|
||||
|
|
@ -581,3 +581,4 @@ const result = await executeNodeFlow(flowId, {
|
|||
- 프론트엔드 플로우 API: `frontend/lib/api/nodeFlows.ts`
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,357 @@
|
|||
# 메일 발송 기능 사용 가이드
|
||||
|
||||
## 개요
|
||||
|
||||
노드 기반 제어관리 시스템을 통해 메일을 발송하는 방법을 설명합니다.
|
||||
화면에서 데이터를 선택하고, 수신자를 지정하여 템플릿 기반의 메일을 발송할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 사전 준비
|
||||
|
||||
### 1.1 메일 계정 등록
|
||||
|
||||
메일 발송을 위해 먼저 SMTP 계정을 등록해야 합니다.
|
||||
|
||||
1. **관리자** > **메일관리** > **계정관리** 이동
|
||||
2. **새 계정 추가** 클릭
|
||||
3. SMTP 정보 입력:
|
||||
- 계정명: 식별용 이름 (예: "회사 공식 메일")
|
||||
- 이메일: 발신자 이메일 주소
|
||||
- SMTP 호스트: 메일 서버 주소 (예: smtp.gmail.com)
|
||||
- SMTP 포트: 포트 번호 (예: 587)
|
||||
- 보안: TLS/SSL 선택
|
||||
- 사용자명/비밀번호: SMTP 인증 정보
|
||||
4. **저장** 후 **테스트 발송**으로 동작 확인
|
||||
|
||||
---
|
||||
|
||||
## 2. 제어관리 설정
|
||||
|
||||
### 2.1 메일 발송 플로우 생성
|
||||
|
||||
**관리자** > **제어관리** > **플로우 관리**에서 새 플로우를 생성합니다.
|
||||
|
||||
#### 기본 구조
|
||||
|
||||
```
|
||||
[테이블 소스] → [메일 발송]
|
||||
```
|
||||
|
||||
#### 노드 구성
|
||||
|
||||
1. **테이블 소스 노드** 추가
|
||||
|
||||
- 데이터 소스: **컨텍스트 데이터** (화면에서 선택한 데이터 사용)
|
||||
- 또는 **테이블 전체 데이터** (주의: 전체 데이터 건수만큼 메일 발송)
|
||||
|
||||
2. **메일 발송 노드** 추가
|
||||
|
||||
- 노드 팔레트 > 외부 실행 > **메일 발송** 드래그
|
||||
|
||||
3. 두 노드 연결 (테이블 소스 → 메일 발송)
|
||||
|
||||
---
|
||||
|
||||
### 2.2 메일 발송 노드 설정
|
||||
|
||||
메일 발송 노드를 클릭하면 우측에 속성 패널이 표시됩니다.
|
||||
|
||||
#### 계정 탭
|
||||
|
||||
| 설정 | 설명 |
|
||||
| -------------- | ----------------------------------- |
|
||||
| 발송 계정 선택 | 사전에 등록한 메일 계정 선택 (필수) |
|
||||
|
||||
#### 메일 탭
|
||||
|
||||
| 설정 | 설명 |
|
||||
| -------------------- | ------------------------------------------------ |
|
||||
| 수신자 컴포넌트 사용 | 체크 시 화면의 수신자 선택 컴포넌트 값 자동 사용 |
|
||||
| 수신자 필드명 | 수신자 변수명 (기본: mailTo) |
|
||||
| 참조 필드명 | 참조 변수명 (기본: mailCc) |
|
||||
| 수신자 (To) | 직접 입력 또는 변수 사용 (예: `{{email}}`) |
|
||||
| 참조 (CC) | 참조 수신자 |
|
||||
| 숨은 참조 (BCC) | 숨은 참조 수신자 |
|
||||
| 우선순위 | 높음 / 보통 / 낮음 |
|
||||
|
||||
#### 본문 탭
|
||||
|
||||
| 설정 | 설명 |
|
||||
| --------- | -------------------------------- |
|
||||
| 제목 | 메일 제목 (변수 사용 가능) |
|
||||
| 본문 형식 | 텍스트 (변수 태그 에디터) / HTML |
|
||||
| 본문 내용 | 메일 본문 (변수 사용 가능) |
|
||||
|
||||
#### 옵션 탭
|
||||
|
||||
| 설정 | 설명 |
|
||||
| ----------- | ------------------- |
|
||||
| 타임아웃 | 발송 제한 시간 (ms) |
|
||||
| 재시도 횟수 | 실패 시 재시도 횟수 |
|
||||
|
||||
---
|
||||
|
||||
### 2.3 변수 사용 방법
|
||||
|
||||
메일 제목과 본문에서 `{{변수명}}` 형식으로 데이터 필드를 참조할 수 있습니다.
|
||||
|
||||
#### 텍스트 모드 (변수 태그 에디터)
|
||||
|
||||
1. 본문 형식을 **텍스트 (변수 태그 에디터)** 선택
|
||||
2. 에디터에서 `@` 또는 `/` 키 입력
|
||||
3. 변수 목록에서 원하는 변수 선택
|
||||
4. 선택된 변수는 파란색 태그로 표시
|
||||
|
||||
#### HTML 모드 (직접 입력)
|
||||
|
||||
```html
|
||||
<h1>주문 확인</h1>
|
||||
<p>안녕하세요 {{customerName}}님,</p>
|
||||
<p>주문번호 {{orderNo}}의 주문이 완료되었습니다.</p>
|
||||
<p>금액: {{totalAmount}}원</p>
|
||||
```
|
||||
|
||||
#### 사용 가능한 변수
|
||||
|
||||
| 변수 | 설명 |
|
||||
| ---------------- | ------------------------ |
|
||||
| `{{timestamp}}` | 메일 발송 시점 |
|
||||
| `{{sourceData}}` | 전체 소스 데이터 (JSON) |
|
||||
| `{{필드명}}` | 테이블 소스의 각 컬럼 값 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 화면 구성
|
||||
|
||||
### 3.1 기본 구조
|
||||
|
||||
메일 발송 화면은 보통 다음과 같이 구성합니다:
|
||||
|
||||
```
|
||||
[부모 화면: 데이터 목록]
|
||||
↓ (모달 열기 버튼)
|
||||
[모달: 수신자 입력 + 발송 버튼]
|
||||
```
|
||||
|
||||
### 3.2 수신자 선택 컴포넌트 배치
|
||||
|
||||
1. **화면관리**에서 모달 화면 편집
|
||||
2. 컴포넌트 팔레트 > **메일 수신자 선택** 드래그
|
||||
3. 컴포넌트 설정:
|
||||
- 수신자 필드명: `mailTo` (메일 발송 노드와 일치)
|
||||
- 참조 필드명: `mailCc` (메일 발송 노드와 일치)
|
||||
|
||||
#### 수신자 선택 기능
|
||||
|
||||
- **내부 사용자**: 회사 직원 목록에서 검색/선택
|
||||
- **외부 이메일**: 직접 이메일 주소 입력
|
||||
- 여러 명 선택 가능 (쉼표로 구분)
|
||||
|
||||
### 3.3 발송 버튼 설정
|
||||
|
||||
1. **버튼** 컴포넌트 추가
|
||||
2. 버튼 설정:
|
||||
- 액션 타입: **제어 실행**
|
||||
- 플로우 선택: 생성한 메일 발송 플로우
|
||||
- 데이터 소스: **자동** 또는 **폼 + 테이블 선택**
|
||||
|
||||
---
|
||||
|
||||
## 4. 전체 흐름 예시
|
||||
|
||||
### 4.1 시나리오: 선택한 주문 건에 대해 고객에게 메일 발송
|
||||
|
||||
#### Step 1: 제어관리 플로우 생성
|
||||
|
||||
```
|
||||
[테이블 소스: 컨텍스트 데이터]
|
||||
↓
|
||||
[메일 발송]
|
||||
- 계정: 회사 공식 메일
|
||||
- 수신자 컴포넌트 사용: 체크
|
||||
- 제목: [주문확인] {{orderNo}} 주문이 완료되었습니다
|
||||
- 본문:
|
||||
안녕하세요 {{customerName}}님,
|
||||
|
||||
주문번호 {{orderNo}}의 주문이 정상 처리되었습니다.
|
||||
|
||||
- 상품명: {{productName}}
|
||||
- 수량: {{quantity}}
|
||||
- 금액: {{totalAmount}}원
|
||||
|
||||
감사합니다.
|
||||
```
|
||||
|
||||
#### Step 2: 부모 화면 (주문 목록)
|
||||
|
||||
- 주문 데이터 테이블
|
||||
- "메일 발송" 버튼
|
||||
- 액션: 모달 열기
|
||||
- 모달 화면: 메일 발송 모달
|
||||
- 선택된 데이터 전달: 체크
|
||||
|
||||
#### Step 3: 모달 화면 (메일 발송)
|
||||
|
||||
- 메일 수신자 선택 컴포넌트
|
||||
- 수신자 (To) 입력
|
||||
- 참조 (CC) 입력
|
||||
- "발송" 버튼
|
||||
- 액션: 제어 실행
|
||||
- 플로우: 메일 발송 플로우
|
||||
|
||||
#### Step 4: 실행 흐름
|
||||
|
||||
1. 사용자가 주문 목록에서 주문 선택
|
||||
2. "메일 발송" 버튼 클릭 → 모달 열림
|
||||
3. 수신자/참조 입력
|
||||
4. "발송" 버튼 클릭
|
||||
5. 제어 실행:
|
||||
- 부모 화면 데이터 (orderNo, customerName 등) + 모달 폼 데이터 (mailTo, mailCc) 병합
|
||||
- 변수 치환 후 메일 발송
|
||||
|
||||
---
|
||||
|
||||
## 5. 데이터 소스별 동작
|
||||
|
||||
### 5.1 컨텍스트 데이터 (권장)
|
||||
|
||||
- 화면에서 **선택한 데이터**만 사용
|
||||
- 선택한 건수만큼 메일 발송
|
||||
|
||||
| 선택 건수 | 메일 발송 수 |
|
||||
| --------- | ------------ |
|
||||
| 1건 | 1통 |
|
||||
| 5건 | 5통 |
|
||||
| 10건 | 10통 |
|
||||
|
||||
### 5.2 테이블 전체 데이터 (주의)
|
||||
|
||||
- 테이블의 **모든 데이터** 사용
|
||||
- 전체 건수만큼 메일 발송
|
||||
|
||||
| 테이블 데이터 | 메일 발송 수 |
|
||||
| ------------- | ------------ |
|
||||
| 100건 | 100통 |
|
||||
| 1000건 | 1000통 |
|
||||
|
||||
**주의사항:**
|
||||
|
||||
- 대량 발송 시 SMTP 서버 rate limit 주의
|
||||
- 테스트 시 반드시 데이터 건수 확인
|
||||
|
||||
---
|
||||
|
||||
## 6. 문제 해결
|
||||
|
||||
### 6.1 메일이 발송되지 않음
|
||||
|
||||
1. **계정 설정 확인**: 메일관리 > 계정관리에서 테스트 발송 확인
|
||||
2. **수신자 확인**: 수신자 이메일 주소가 올바른지 확인
|
||||
3. **플로우 연결 확인**: 테이블 소스 → 메일 발송 노드가 연결되어 있는지 확인
|
||||
|
||||
### 6.2 변수가 치환되지 않음
|
||||
|
||||
1. **변수명 확인**: `{{변수명}}`에서 변수명이 테이블 컬럼명과 일치하는지 확인
|
||||
2. **데이터 소스 확인**: 테이블 소스 노드가 올바른 데이터를 가져오는지 확인
|
||||
3. **데이터 전달 확인**: 부모 화면 → 모달로 데이터가 전달되는지 확인
|
||||
|
||||
### 6.3 수신자 컴포넌트 값이 전달되지 않음
|
||||
|
||||
1. **필드명 일치 확인**:
|
||||
- 수신자 컴포넌트의 필드명과 메일 발송 노드의 필드명이 일치해야 함
|
||||
- 기본값: `mailTo`, `mailCc`
|
||||
2. **수신자 컴포넌트 사용 체크**: 메일 발송 노드에서 "수신자 컴포넌트 사용" 활성화
|
||||
|
||||
### 6.4 부모 화면 데이터가 메일에 포함되지 않음
|
||||
|
||||
1. **모달 열기 설정 확인**: "선택된 데이터 전달" 옵션 활성화
|
||||
2. **데이터 소스 설정 확인**: 발송 버튼의 데이터 소스가 "자동" 또는 "폼 + 테이블 선택"인지 확인
|
||||
|
||||
---
|
||||
|
||||
## 7. 고급 기능
|
||||
|
||||
### 7.1 조건부 메일 발송
|
||||
|
||||
조건 분기 노드를 사용하여 특정 조건에서만 메일을 발송할 수 있습니다.
|
||||
|
||||
```
|
||||
[테이블 소스]
|
||||
↓
|
||||
[조건 분기: status === 'approved']
|
||||
↓ (true)
|
||||
[메일 발송: 승인 알림]
|
||||
```
|
||||
|
||||
### 7.2 다중 수신자 처리
|
||||
|
||||
수신자 필드에 쉼표로 구분하여 여러 명에게 동시 발송:
|
||||
|
||||
```
|
||||
{{managerEmail}}, {{teamLeadEmail}}, external@example.com
|
||||
```
|
||||
|
||||
### 7.3 HTML 템플릿 활용
|
||||
|
||||
본문 형식을 HTML로 설정하면 풍부한 형식의 메일을 보낼 수 있습니다:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
.header {
|
||||
background: #4a90d9;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
}
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
.footer {
|
||||
background: #f5f5f5;
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>주문 확인</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>안녕하세요 <strong>{{customerName}}</strong>님,</p>
|
||||
<p>주문번호 <strong>{{orderNo}}</strong>의 주문이 완료되었습니다.</p>
|
||||
<table>
|
||||
<tr>
|
||||
<td>상품명</td>
|
||||
<td>{{productName}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>금액</td>
|
||||
<td>{{totalAmount}}원</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="footer">본 메일은 자동 발송되었습니다.</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 체크리스트
|
||||
|
||||
메일 발송 기능 구현 시 확인 사항:
|
||||
|
||||
- [ ] 메일 계정이 등록되어 있는가?
|
||||
- [ ] 메일 계정 테스트 발송이 성공하는가?
|
||||
- [ ] 제어관리에 메일 발송 플로우가 생성되어 있는가?
|
||||
- [ ] 테이블 소스 노드의 데이터 소스가 올바르게 설정되어 있는가?
|
||||
- [ ] 메일 발송 노드에서 계정이 선택되어 있는가?
|
||||
- [ ] 수신자 컴포넌트 사용 시 필드명이 일치하는가?
|
||||
- [ ] 변수명이 테이블 컬럼명과 일치하는가?
|
||||
- [ ] 부모 화면에서 모달로 데이터가 전달되는가?
|
||||
- [ ] 발송 버튼의 데이터 소스가 올바르게 설정되어 있는가?
|
||||
|
||||
|
|
@ -5,14 +5,9 @@ import { Input } from "@/components/ui/input";
|
|||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Palette, Type, Square, Box } from "lucide-react";
|
||||
import { Palette, Type, Square } from "lucide-react";
|
||||
import { ComponentStyle } from "@/types/screen";
|
||||
|
||||
interface StyleEditorProps {
|
||||
style: ComponentStyle;
|
||||
onStyleChange: (style: ComponentStyle) => void;
|
||||
className?: string;
|
||||
}
|
||||
import { ColorPickerWithTransparent } from "./common/ColorPickerWithTransparent";
|
||||
|
||||
export default function StyleEditor({ style, onStyleChange, className }: StyleEditorProps) {
|
||||
const [localStyle, setLocalStyle] = useState<ComponentStyle>(style || {});
|
||||
|
|
@ -80,28 +75,18 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="borderColor" className="text-xs font-medium">
|
||||
색상
|
||||
</Label>
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
id="borderColor"
|
||||
type="color"
|
||||
value={localStyle.borderColor || "#000000"}
|
||||
onChange={(e) => handleStyleChange("borderColor", e.target.value)}
|
||||
className="h-6 w-12 p-1"
|
||||
className="text-xs"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={localStyle.borderColor || "#000000"}
|
||||
onChange={(e) => handleStyleChange("borderColor", e.target.value)}
|
||||
placeholder="#000000"
|
||||
className="h-6 flex-1 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<ColorPickerWithTransparent
|
||||
id="borderColor"
|
||||
value={localStyle.borderColor}
|
||||
onChange={(value) => handleStyleChange("borderColor", value)}
|
||||
defaultColor="#e5e7eb"
|
||||
placeholder="#e5e7eb"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="borderRadius" className="text-xs font-medium">
|
||||
|
|
@ -132,23 +117,13 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
|||
<Label htmlFor="backgroundColor" className="text-xs font-medium">
|
||||
색상
|
||||
</Label>
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
id="backgroundColor"
|
||||
type="color"
|
||||
value={localStyle.backgroundColor || "#ffffff"}
|
||||
onChange={(e) => handleStyleChange("backgroundColor", e.target.value)}
|
||||
className="h-6 w-12 p-1"
|
||||
className="text-xs"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={localStyle.backgroundColor || "#ffffff"}
|
||||
onChange={(e) => handleStyleChange("backgroundColor", e.target.value)}
|
||||
placeholder="#ffffff"
|
||||
className="h-6 flex-1 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<ColorPickerWithTransparent
|
||||
id="backgroundColor"
|
||||
value={localStyle.backgroundColor}
|
||||
onChange={(value) => handleStyleChange("backgroundColor", value)}
|
||||
defaultColor="#ffffff"
|
||||
placeholder="#ffffff"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
|
|
@ -178,28 +153,18 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
|||
</div>
|
||||
<Separator className="my-1.5" />
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="color" className="text-xs font-medium">
|
||||
색상
|
||||
</Label>
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
id="color"
|
||||
type="color"
|
||||
value={localStyle.color || "#000000"}
|
||||
onChange={(e) => handleStyleChange("color", e.target.value)}
|
||||
className="h-6 w-12 p-1"
|
||||
className="text-xs"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={localStyle.color || "#000000"}
|
||||
onChange={(e) => handleStyleChange("color", e.target.value)}
|
||||
placeholder="#000000"
|
||||
className="h-6 flex-1 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<ColorPickerWithTransparent
|
||||
id="color"
|
||||
value={localStyle.color}
|
||||
onChange={(value) => handleStyleChange("color", value)}
|
||||
defaultColor="#000000"
|
||||
placeholder="#000000"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="fontSize" className="text-xs font-medium">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
"use client";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface ColorPickerWithTransparentProps {
|
||||
id?: string;
|
||||
value: string | undefined;
|
||||
onChange: (value: string | undefined) => void;
|
||||
defaultColor?: string;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 투명 옵션이 포함된 색상 선택 컴포넌트
|
||||
* - 색상 선택기 (color input)
|
||||
* - 텍스트 입력 (HEX 값 직접 입력)
|
||||
* - 투명 버튼 (투명/색상 토글)
|
||||
*/
|
||||
export function ColorPickerWithTransparent({
|
||||
id,
|
||||
value,
|
||||
onChange,
|
||||
defaultColor = "#000000",
|
||||
placeholder = "#000000",
|
||||
className = "",
|
||||
}: ColorPickerWithTransparentProps) {
|
||||
const isTransparent = value === "transparent" || value === "";
|
||||
const displayValue = isTransparent ? defaultColor : (value || defaultColor);
|
||||
|
||||
return (
|
||||
<div className={`flex gap-1 items-center ${className}`}>
|
||||
{/* 색상 선택기 */}
|
||||
<div className="relative">
|
||||
<Input
|
||||
id={id}
|
||||
type="color"
|
||||
value={displayValue}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="h-6 w-12 p-1 text-xs cursor-pointer"
|
||||
disabled={isTransparent}
|
||||
/>
|
||||
{/* 투명 표시 오버레이 (체커보드 패턴) */}
|
||||
{isTransparent && (
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center border rounded pointer-events-none"
|
||||
style={{
|
||||
backgroundImage: "linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(-45deg, transparent 75%, #ccc 75%)",
|
||||
backgroundSize: "8px 8px",
|
||||
backgroundPosition: "0 0, 0 4px, 4px -4px, -4px 0px"
|
||||
}}
|
||||
>
|
||||
<span className="text-[8px] font-bold text-gray-600 bg-white/80 px-0.5 rounded">투명</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 텍스트 입력 */}
|
||||
<Input
|
||||
type="text"
|
||||
value={isTransparent ? "transparent" : (value || "")}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
if (newValue === "transparent" || newValue === "") {
|
||||
onChange("transparent");
|
||||
} else {
|
||||
onChange(newValue);
|
||||
}
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
className="h-6 flex-1 text-xs"
|
||||
/>
|
||||
|
||||
{/* 투명 버튼 */}
|
||||
<Button
|
||||
type="button"
|
||||
variant={isTransparent ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="h-6 px-2 text-[10px] shrink-0"
|
||||
onClick={() => {
|
||||
if (isTransparent) {
|
||||
onChange(defaultColor);
|
||||
} else {
|
||||
onChange("transparent");
|
||||
}
|
||||
}}
|
||||
title={isTransparent ? "색상 선택" : "투명으로 설정"}
|
||||
>
|
||||
{isTransparent ? "색상" : "투명"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ColorPickerWithTransparent;
|
||||
|
||||
|
|
@ -6,6 +6,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { ColorPickerWithTransparent } from "../common/ColorPickerWithTransparent";
|
||||
|
||||
interface CardConfigPanelProps {
|
||||
component: ComponentData;
|
||||
|
|
@ -93,11 +94,12 @@ export const CardConfigPanel: React.FC<CardConfigPanelProps> = ({ component, onU
|
|||
{/* 배경색 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="background-color">배경색</Label>
|
||||
<Input
|
||||
<ColorPickerWithTransparent
|
||||
id="background-color"
|
||||
type="color"
|
||||
value={config.backgroundColor || "#ffffff"}
|
||||
onChange={(e) => handleConfigChange("backgroundColor", e.target.value)}
|
||||
value={config.backgroundColor}
|
||||
onChange={(value) => handleConfigChange("backgroundColor", value)}
|
||||
defaultColor="#ffffff"
|
||||
placeholder="#ffffff"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input";
|
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { ColorPickerWithTransparent } from "../common/ColorPickerWithTransparent";
|
||||
|
||||
interface DashboardConfigPanelProps {
|
||||
component: ComponentData;
|
||||
|
|
@ -124,11 +125,12 @@ export const DashboardConfigPanel: React.FC<DashboardConfigPanelProps> = ({ comp
|
|||
{/* 배경색 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="background-color">배경색</Label>
|
||||
<Input
|
||||
<ColorPickerWithTransparent
|
||||
id="background-color"
|
||||
type="color"
|
||||
value={config.backgroundColor || "#f8f9fa"}
|
||||
onChange={(e) => handleConfigChange("backgroundColor", e.target.value)}
|
||||
value={config.backgroundColor}
|
||||
onChange={(value) => handleConfigChange("backgroundColor", value)}
|
||||
defaultColor="#f8f9fa"
|
||||
placeholder="#f8f9fa"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Label } from "@/components/ui/label";
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { ColorPickerWithTransparent } from "../common/ColorPickerWithTransparent";
|
||||
|
||||
interface ProgressBarConfigPanelProps {
|
||||
component: ComponentData;
|
||||
|
|
@ -52,11 +53,12 @@ export const ProgressBarConfigPanel: React.FC<ProgressBarConfigPanelProps> = ({
|
|||
|
||||
<div>
|
||||
<Label htmlFor="progress-color">진행률 색상</Label>
|
||||
<Input
|
||||
<ColorPickerWithTransparent
|
||||
id="progress-color"
|
||||
type="color"
|
||||
value={config.color || "#3b82f6"}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.color", e.target.value)}
|
||||
value={config.color}
|
||||
onChange={(value) => onUpdateProperty("componentConfig.color", value)}
|
||||
defaultColor="#3b82f6"
|
||||
placeholder="#3b82f6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { Separator } from "@/components/ui/separator";
|
|||
import { Slider } from "@/components/ui/slider";
|
||||
import { Grid3X3, RotateCcw, Eye, EyeOff, Zap } from "lucide-react";
|
||||
import { GridSettings, ScreenResolution } from "@/types/screen";
|
||||
import { ColorPickerWithTransparent } from "../common/ColorPickerWithTransparent";
|
||||
|
||||
interface GridPanelProps {
|
||||
gridSettings: GridSettings;
|
||||
|
|
@ -105,20 +106,13 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
|||
<Label htmlFor="gridColor" className="text-xs font-medium">
|
||||
격자 색상
|
||||
</Label>
|
||||
<div className="mt-1 flex items-center space-x-2">
|
||||
<Input
|
||||
<div className="mt-1">
|
||||
<ColorPickerWithTransparent
|
||||
id="gridColor"
|
||||
type="color"
|
||||
value={gridSettings.gridColor || "#d1d5db"}
|
||||
onChange={(e) => updateSetting("gridColor", e.target.value)}
|
||||
className="h-8 w-12 rounded border p-1"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={gridSettings.gridColor || "#d1d5db"}
|
||||
onChange={(e) => updateSetting("gridColor", e.target.value)}
|
||||
value={gridSettings.gridColor}
|
||||
onChange={(value) => updateSetting("gridColor", value)}
|
||||
defaultColor="#d1d5db"
|
||||
placeholder="#d1d5db"
|
||||
className="flex-1 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import {
|
|||
getBaseInputType,
|
||||
getDefaultDetailType,
|
||||
} from "@/types/input-type-mapping";
|
||||
import { ColorPickerWithTransparent } from "../common/ColorPickerWithTransparent";
|
||||
|
||||
// DataTableConfigPanel을 위한 안정화된 래퍼 컴포넌트
|
||||
const DataTableConfigPanelWrapper: React.FC<{
|
||||
|
|
@ -1092,17 +1093,18 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
|||
<Label htmlFor="labelColor" className="text-sm font-medium">
|
||||
색상
|
||||
</Label>
|
||||
<Input
|
||||
id="labelColor"
|
||||
type="color"
|
||||
value={localInputs.labelColor}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
setLocalInputs((prev) => ({ ...prev, labelColor: newValue }));
|
||||
onUpdateProperty("style.labelColor", newValue);
|
||||
}}
|
||||
className="mt-1 h-8"
|
||||
/>
|
||||
<div className="mt-1">
|
||||
<ColorPickerWithTransparent
|
||||
id="labelColor"
|
||||
value={localInputs.labelColor}
|
||||
onChange={(value) => {
|
||||
setLocalInputs((prev) => ({ ...prev, labelColor: value || "" }));
|
||||
onUpdateProperty("style.labelColor", value);
|
||||
}}
|
||||
defaultColor="#212121"
|
||||
placeholder="#212121"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { Separator } from "@/components/ui/separator";
|
|||
import { LayoutRow } from "@/types/grid-system";
|
||||
import { GapPreset, GAP_PRESETS } from "@/lib/constants/columnSpans";
|
||||
import { Rows, AlignHorizontalJustifyCenter, AlignVerticalJustifyCenter } from "lucide-react";
|
||||
import { ColorPickerWithTransparent } from "../common/ColorPickerWithTransparent";
|
||||
|
||||
interface RowSettingsPanelProps {
|
||||
row: LayoutRow;
|
||||
|
|
@ -224,26 +225,12 @@ export const RowSettingsPanel: React.FC<RowSettingsPanelProps> = ({ row, onUpdat
|
|||
{/* 배경색 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">배경색</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="color"
|
||||
value={row.backgroundColor || "#ffffff"}
|
||||
onChange={(e) => onUpdateRow({ backgroundColor: e.target.value })}
|
||||
className="h-10 w-20 cursor-pointer p-1"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={row.backgroundColor || ""}
|
||||
onChange={(e) => onUpdateRow({ backgroundColor: e.target.value })}
|
||||
placeholder="#ffffff"
|
||||
className="flex-1"
|
||||
/>
|
||||
{row.backgroundColor && (
|
||||
<Button variant="ghost" size="sm" onClick={() => onUpdateRow({ backgroundColor: undefined })}>
|
||||
초기화
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<ColorPickerWithTransparent
|
||||
value={row.backgroundColor}
|
||||
onChange={(value) => onUpdateRow({ backgroundColor: value })}
|
||||
defaultColor="#ffffff"
|
||||
placeholder="#ffffff"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import {
|
|||
import { ButtonConfigPanel } from "../config-panels/ButtonConfigPanel";
|
||||
import { CardConfigPanel } from "../config-panels/CardConfigPanel";
|
||||
import { DashboardConfigPanel } from "../config-panels/DashboardConfigPanel";
|
||||
import { ColorPickerWithTransparent } from "../common/ColorPickerWithTransparent";
|
||||
import { StatsCardConfigPanel } from "../config-panels/StatsCardConfigPanel";
|
||||
|
||||
// ComponentRegistry import (동적 ConfigPanel 가져오기용)
|
||||
|
|
@ -603,13 +604,13 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
{selectedComponent.componentConfig?.backgroundColor === "custom" && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">커스텀 색상</Label>
|
||||
<Input
|
||||
type="color"
|
||||
value={selectedComponent.componentConfig?.customColor || "#f0f0f0"}
|
||||
onChange={(e) => {
|
||||
handleUpdateProperty(selectedComponent.id, "componentConfig.customColor", e.target.value);
|
||||
<ColorPickerWithTransparent
|
||||
value={selectedComponent.componentConfig?.customColor}
|
||||
onChange={(value) => {
|
||||
handleUpdateProperty(selectedComponent.id, "componentConfig.customColor", value);
|
||||
}}
|
||||
className="h-9"
|
||||
defaultColor="#f0f0f0"
|
||||
placeholder="#f0f0f0"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -882,12 +883,11 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">색상</Label>
|
||||
<Input
|
||||
type="color"
|
||||
value={selectedComponent.style?.labelColor || "#212121"}
|
||||
onChange={(e) => handleUpdate("style.labelColor", e.target.value)}
|
||||
className="h-6 w-full px-2 py-0 text-xs"
|
||||
className="text-xs"
|
||||
<ColorPickerWithTransparent
|
||||
value={selectedComponent.style?.labelColor}
|
||||
onChange={(value) => handleUpdate("style.labelColor", value)}
|
||||
defaultColor="#212121"
|
||||
placeholder="#212121"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1683,3 +1683,4 @@ const 출고등록_설정: ScreenSplitPanel = {
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -530,3 +530,4 @@ const { data: config } = await getScreenSplitPanel(screenId);
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -517,3 +517,4 @@ function ScreenViewPage() {
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue