690b85805c
- ECR 관리: wace 의 ecrList/Form/Detail JSP 와 동일하게 5개 필터(연도/기종/요청/작성자/상태), 변경전/후 2분할 모달, 작성중(0000100)만 삭제·수정 허용, 컬럼 순서/라벨 wace 일치 - ECR 스키마 wace 풀세트 동기화: ecr_mng 컬럼폭 확장, product_mgmt 16컬럼, part_mng 52컬럼, user_info 22컬럼, comm_code 보강(id/code_cd/ext_val), code_name(varchar) 함수, seq_ecr_no setval(33) 정렬 - wace_plm public.comm_code 733행 시드: src/seed/wace_comm_code.sql 추출 + 부팅 시 자동 적재 (writer='system-seed' placeholder 자동 정리, 무중단 재적재 엔드포인트 /comm-code-seed) - wace_plm 데이터 import 풀스키마: PRODUCT(7→16), PART(6→52), USER_INFO·COMM_CODE 신규 - 공통코드 관리 화면: 제목/설명 축소, 카테고리·코드 카드 → 컴팩트 리스트, 활성 토글 점, 계층 배지 톤다운, hover 시 액션 노출 - 테이블 타입 관리 — 좌측: 한 줄 리스트 + 알파벳 인덱스 sticky 헤더 - 테이블 타입 관리 — 우측: 타입 카드 그리드 → 그룹 셀렉트(기본/참조/자동/첨부/표시변형), 표시이름 제거 + 코멘트(description) Textarea 신설(화면관리에서 기본 라벨로 활용), 시스템 자동 생성 컬럼(id/company_code/writer/created_date/updated_date) 잠금, 표시옵션/고급설정(필수·읽기·기본값·최대길이) 제거 — 화면관리로 이관 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
311 lines
19 KiB
TypeScript
311 lines
19 KiB
TypeScript
/**
|
|
* ECR(Engineering Change Request) 관련 테이블 idempotent 마이그레이션
|
|
*
|
|
* - wace_plm 의 ECR_MNG / PRODUCT_MGMT / PART_MNG / COMM_CODE / USER_INFO 스키마와
|
|
* 1:1 매칭되도록 컬럼 명세를 동기화 (CREATE TABLE IF NOT EXISTS + ADD COLUMN IF NOT EXISTS)
|
|
* - wace_plm 의 code_name(varchar) 함수도 그대로 재현 (mapper SQL 에서 사용)
|
|
* - 시퀀스 seq_ecr_no 도 동일 보장
|
|
* - 부팅 시 1회 실행 (멱등)
|
|
*
|
|
* 주의: 데이터 자체의 import 는 WacePlmDataImportService (POST /api/wace-import/*) 가 담당.
|
|
* 본 마이그레이션은 "동일 스키마" 보장만 한다.
|
|
*/
|
|
import { getPool } from "../database/db";
|
|
import { logger } from "../utils/logger";
|
|
|
|
const STATEMENTS: string[] = [
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// 1) ECR_MNG (wace_plm dbexport 기준 컬럼 명세와 정확히 일치)
|
|
//
|
|
// wace_plm DDL:
|
|
// objid integer NOT NULL, ecr_no varchar(100), product_objid integer,
|
|
// upg_no varchar(100), part_objid integer, request_cd varchar(100),
|
|
// title varchar(1000), writer varchar(100), status_cd varchar(100),
|
|
// before_contents varchar(4000), after_contents varchar(4000),
|
|
// reg_date timestamp, check_user_id varchar(100), check_date timestamp
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
`CREATE TABLE IF NOT EXISTS ecr_mng (
|
|
objid BIGINT PRIMARY KEY,
|
|
ecr_no VARCHAR(100),
|
|
product_objid BIGINT,
|
|
upg_no VARCHAR(100),
|
|
part_objid BIGINT,
|
|
request_cd VARCHAR(100),
|
|
title VARCHAR(1000),
|
|
writer VARCHAR(100),
|
|
status_cd VARCHAR(100) DEFAULT '0000100',
|
|
before_contents VARCHAR(4000),
|
|
after_contents VARCHAR(4000),
|
|
reg_date TIMESTAMP DEFAULT NOW(),
|
|
check_user_id VARCHAR(100),
|
|
check_date TIMESTAMP
|
|
)`,
|
|
// 기존 환경(이전 마이그레이션으로 짧은 길이로 만들어진 경우)을 위해 컬럼 폭 확장 (멱등)
|
|
`ALTER TABLE ecr_mng ALTER COLUMN ecr_no TYPE VARCHAR(100) USING ecr_no::VARCHAR(100)`,
|
|
`ALTER TABLE ecr_mng ALTER COLUMN upg_no TYPE VARCHAR(100) USING upg_no::VARCHAR(100)`,
|
|
`ALTER TABLE ecr_mng ALTER COLUMN request_cd TYPE VARCHAR(100) USING request_cd::VARCHAR(100)`,
|
|
`ALTER TABLE ecr_mng ALTER COLUMN title TYPE VARCHAR(1000) USING title::VARCHAR(1000)`,
|
|
`ALTER TABLE ecr_mng ALTER COLUMN writer TYPE VARCHAR(100) USING writer::VARCHAR(100)`,
|
|
`ALTER TABLE ecr_mng ALTER COLUMN status_cd TYPE VARCHAR(100) USING status_cd::VARCHAR(100)`,
|
|
`ALTER TABLE ecr_mng ALTER COLUMN before_contents TYPE VARCHAR(4000) USING before_contents::VARCHAR(4000)`,
|
|
`ALTER TABLE ecr_mng ALTER COLUMN after_contents TYPE VARCHAR(4000) USING after_contents::VARCHAR(4000)`,
|
|
`ALTER TABLE ecr_mng ALTER COLUMN check_user_id TYPE VARCHAR(100) USING check_user_id::VARCHAR(100)`,
|
|
// 신규로 만든 환경에 company_code 가 떨어졌을 수 있어 제거 시도 없이 그냥 두되, 누락된 컬럼만 보충
|
|
`ALTER TABLE ecr_mng ADD COLUMN IF NOT EXISTS upg_no VARCHAR(100)`,
|
|
`ALTER TABLE ecr_mng ADD COLUMN IF NOT EXISTS check_user_id VARCHAR(100)`,
|
|
`ALTER TABLE ecr_mng ADD COLUMN IF NOT EXISTS check_date TIMESTAMP`,
|
|
|
|
`CREATE INDEX IF NOT EXISTS idx_ecr_mng_status ON ecr_mng (status_cd)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_ecr_mng_writer ON ecr_mng (writer)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_ecr_mng_reg_date ON ecr_mng (reg_date DESC)`,
|
|
|
|
// ECR_NO 시퀀스 — wace_plm 도 동일하게 별도 SEQUENCE 사용
|
|
`CREATE SEQUENCE IF NOT EXISTS seq_ecr_no START WITH 1 INCREMENT BY 1`,
|
|
// wace_plm 운영본은 33 까지 발번되어 있음. 우리 seq 가 더 작으면 그 값까지 끌어올려서
|
|
// 채번 충돌(ON CONFLICT 로 UPDATE 분기 타는 사고)을 방지.
|
|
`SELECT setval('seq_ecr_no', GREATEST((SELECT last_value FROM seq_ecr_no), 33))`,
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// 2) PRODUCT_MGMT (wace_plm dbexport 컬럼 풀세트)
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
`CREATE TABLE IF NOT EXISTS product_mgmt (
|
|
objid BIGINT PRIMARY KEY,
|
|
product_category VARCHAR(100),
|
|
product_type VARCHAR(100),
|
|
product_grade VARCHAR(100),
|
|
product_ton VARCHAR(100),
|
|
product_boom VARCHAR(100),
|
|
product_vehicle VARCHAR(100),
|
|
product_code VARCHAR(100),
|
|
production_flag VARCHAR(100),
|
|
regdate TIMESTAMP,
|
|
writer VARCHAR(100),
|
|
contents TEXT,
|
|
price VARCHAR,
|
|
product_name VARCHAR,
|
|
product_name_code VARCHAR,
|
|
note VARCHAR
|
|
)`,
|
|
// 기존 좁은 컬럼이면 폭 확장
|
|
`ALTER TABLE product_mgmt ALTER COLUMN product_code TYPE VARCHAR(100) USING product_code::VARCHAR(100)`,
|
|
`ALTER TABLE product_mgmt ALTER COLUMN product_type TYPE VARCHAR(100) USING product_type::VARCHAR(100)`,
|
|
`ALTER TABLE product_mgmt ALTER COLUMN writer TYPE VARCHAR(100) USING writer::VARCHAR(100)`,
|
|
// 누락 컬럼 보충 (이전 마이그레이션은 일부만 만들었음)
|
|
`ALTER TABLE product_mgmt ADD COLUMN IF NOT EXISTS product_category VARCHAR(100)`,
|
|
`ALTER TABLE product_mgmt ADD COLUMN IF NOT EXISTS product_grade VARCHAR(100)`,
|
|
`ALTER TABLE product_mgmt ADD COLUMN IF NOT EXISTS product_ton VARCHAR(100)`,
|
|
`ALTER TABLE product_mgmt ADD COLUMN IF NOT EXISTS product_boom VARCHAR(100)`,
|
|
`ALTER TABLE product_mgmt ADD COLUMN IF NOT EXISTS product_vehicle VARCHAR(100)`,
|
|
`ALTER TABLE product_mgmt ADD COLUMN IF NOT EXISTS contents TEXT`,
|
|
`ALTER TABLE product_mgmt ADD COLUMN IF NOT EXISTS price VARCHAR`,
|
|
`ALTER TABLE product_mgmt ADD COLUMN IF NOT EXISTS product_name_code VARCHAR`,
|
|
`ALTER TABLE product_mgmt ADD COLUMN IF NOT EXISTS note VARCHAR`,
|
|
`CREATE INDEX IF NOT EXISTS idx_product_mgmt_code ON product_mgmt (product_code)`,
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// 3) PART_MNG (wace_plm dbexport 컬럼 풀세트 — 50+ 컬럼)
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
`CREATE TABLE IF NOT EXISTS part_mng (
|
|
objid BIGINT PRIMARY KEY,
|
|
product_mgmt_objid VARCHAR(100),
|
|
upg_no VARCHAR(100),
|
|
part_no VARCHAR(100),
|
|
part_name VARCHAR(100),
|
|
unit VARCHAR(50),
|
|
qty VARCHAR(50),
|
|
spec VARCHAR(100),
|
|
material VARCHAR(100),
|
|
weight VARCHAR(50),
|
|
part_type VARCHAR(100),
|
|
remark VARCHAR(1000),
|
|
es_spec VARCHAR(100),
|
|
ms_spec VARCHAR(100),
|
|
change_option VARCHAR(50),
|
|
design_apply_point VARCHAR(50),
|
|
management_flag VARCHAR(50),
|
|
revision VARCHAR(50),
|
|
status VARCHAR(30),
|
|
reg_date TIMESTAMP,
|
|
edit_date TIMESTAMP,
|
|
writer VARCHAR(30),
|
|
is_last VARCHAR(5),
|
|
eo_no VARCHAR,
|
|
eo_temp VARCHAR,
|
|
excel_upload_seq INTEGER,
|
|
sourcing_code VARCHAR,
|
|
sub_material VARCHAR(100),
|
|
parent_part_no VARCHAR,
|
|
design_date VARCHAR,
|
|
eo_date VARCHAR,
|
|
deploy_date TIMESTAMP,
|
|
thickness VARCHAR,
|
|
width VARCHAR,
|
|
height VARCHAR,
|
|
out_diameter VARCHAR,
|
|
in_diameter VARCHAR,
|
|
length VARCHAR,
|
|
supply_code VARCHAR,
|
|
change_type VARCHAR,
|
|
contract_objid VARCHAR,
|
|
maker VARCHAR,
|
|
post_processing VARCHAR,
|
|
material_code VARCHAR,
|
|
code1 VARCHAR,
|
|
code2 VARCHAR,
|
|
code3 VARCHAR,
|
|
code4 VARCHAR,
|
|
code5 VARCHAR,
|
|
major_category VARCHAR,
|
|
sub_category VARCHAR,
|
|
is_new VARCHAR(5),
|
|
is_longd VARCHAR(5)
|
|
)`,
|
|
// 기존 part_mng 가 작은 스키마였다면 누락 컬럼 보충 (멱등 ADD COLUMN IF NOT EXISTS)
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS product_mgmt_objid VARCHAR(100)`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS upg_no VARCHAR(100)`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS unit VARCHAR(50)`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS qty VARCHAR(50)`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS spec VARCHAR(100)`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS material VARCHAR(100)`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS weight VARCHAR(50)`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS remark VARCHAR(1000)`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS es_spec VARCHAR(100)`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS ms_spec VARCHAR(100)`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS change_option VARCHAR(50)`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS design_apply_point VARCHAR(50)`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS management_flag VARCHAR(50)`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS revision VARCHAR(50)`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS reg_date TIMESTAMP`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS edit_date TIMESTAMP`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS is_last VARCHAR(5)`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS eo_no VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS eo_temp VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS excel_upload_seq INTEGER`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS sourcing_code VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS sub_material VARCHAR(100)`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS parent_part_no VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS design_date VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS eo_date VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS deploy_date TIMESTAMP`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS thickness VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS width VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS height VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS out_diameter VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS in_diameter VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS length VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS supply_code VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS change_type VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS contract_objid VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS maker VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS post_processing VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS material_code VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS code1 VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS code2 VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS code3 VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS code4 VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS code5 VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS major_category VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS sub_category VARCHAR`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS is_new VARCHAR(5)`,
|
|
`ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS is_longd VARCHAR(5)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_part_mng_no ON part_mng (part_no)`,
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// 4) COMM_CODE (wace_plm 명세 보충 — 우리 시스템은 이미 일부 컬럼만 가졌을 수 있음)
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
`ALTER TABLE comm_code ADD COLUMN IF NOT EXISTS id VARCHAR(100)`,
|
|
`ALTER TABLE comm_code ADD COLUMN IF NOT EXISTS code_cd VARCHAR(100)`,
|
|
`ALTER TABLE comm_code ADD COLUMN IF NOT EXISTS ext_val VARCHAR(10)`,
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// 5) USER_INFO (wace_plm 의 USER_INFO 와 동일하게 만들지만, 우리 user 테이블과는 별도)
|
|
// - mapper SQL 이 USER_INFO 를 직접 참조 (writer_name, check_name 서브쿼리)
|
|
// - import 시 wace_plm.USER_INFO → 우리 user_info 에 그대로 적재
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
`CREATE TABLE IF NOT EXISTS user_info (
|
|
sabun VARCHAR(1024),
|
|
user_id VARCHAR(1024) PRIMARY KEY,
|
|
user_password VARCHAR(1024),
|
|
user_name VARCHAR(1024),
|
|
user_name_eng VARCHAR(1024),
|
|
user_name_cn VARCHAR(1024),
|
|
dept_code VARCHAR(1024),
|
|
dept_name VARCHAR(1024),
|
|
position_code VARCHAR(1024),
|
|
position_name VARCHAR(1024),
|
|
email VARCHAR(1024),
|
|
tel VARCHAR(1024),
|
|
cell_phone VARCHAR(1024),
|
|
user_type VARCHAR(1024),
|
|
user_type_name VARCHAR(1024),
|
|
regdate TIMESTAMP,
|
|
data_type VARCHAR(64),
|
|
status VARCHAR(32),
|
|
end_date TIMESTAMP,
|
|
fax_no VARCHAR,
|
|
partner_objid VARCHAR,
|
|
rank VARCHAR
|
|
)`,
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// 6) public.code_name(varchar) 함수
|
|
// wace_plm mapper 들이 ECR 등에서 `code_name(STATUS_CD)` 호출 — 동일 함수 정의
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
`CREATE OR REPLACE FUNCTION public.code_name(v_code_id varchar) RETURNS varchar
|
|
LANGUAGE plpgsql AS $$
|
|
DECLARE v_code_name varchar;
|
|
BEGIN
|
|
SELECT code_name INTO v_code_name FROM comm_code WHERE code_id = v_code_id LIMIT 1;
|
|
RETURN v_code_name;
|
|
END;
|
|
$$`,
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// 7) 공통코드 시드 — 0000090(설변요청) / 0000099(ECR상태)
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
`DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM comm_code WHERE code_id='0000090') THEN
|
|
INSERT INTO comm_code (objid, code_id, parent_code_id, code_name, sort_order, status, regdate, writer)
|
|
VALUES (extract(epoch FROM now())::BIGINT, '0000090', NULL, '설변요청 코드', 0, 'active', NOW(), 'system-seed');
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM comm_code WHERE parent_code_id='0000090') THEN
|
|
INSERT INTO comm_code (objid, code_id, parent_code_id, code_name, sort_order, status, regdate, writer) VALUES
|
|
(extract(epoch FROM now())::BIGINT + 1, '0090001', '0000090', '설계오류', 1, 'active', NOW(), 'system-seed'),
|
|
(extract(epoch FROM now())::BIGINT + 2, '0090002', '0000090', '품질개선', 2, 'active', NOW(), 'system-seed'),
|
|
(extract(epoch FROM now())::BIGINT + 3, '0090003', '0000090', '원가절감', 3, 'active', NOW(), 'system-seed'),
|
|
(extract(epoch FROM now())::BIGINT + 4, '0090004', '0000090', '고객요청', 4, 'active', NOW(), 'system-seed'),
|
|
(extract(epoch FROM now())::BIGINT + 5, '0090005', '0000090', '법규대응', 5, 'active', NOW(), 'system-seed'),
|
|
(extract(epoch FROM now())::BIGINT + 6, '0090006', '0000090', '부품단종', 6, 'active', NOW(), 'system-seed');
|
|
END IF;
|
|
END $$`,
|
|
|
|
// 0000099 / 자식(0000100=작성중, 0000101=결재중, 0000107=반려, 0000102=적용완료) 는 wace_plm
|
|
// 원본에 그대로 존재하므로 별도 시드하지 않는다 (wace_comm_code.sql 시드 로더가 처리).
|
|
// wace 가 미보유한 0000090(설변요청) 만 위에서 시드.
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// 8) ECR 메뉴 활성화 (기존 menu_info 100045 정규화)
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
`UPDATE menu_info SET menu_url = '/COMPANY_16/ecr/ecr', status = 'active'
|
|
WHERE objid = '100045' AND (menu_url IS NULL OR menu_url = '')`,
|
|
];
|
|
|
|
export async function ensureEcrTables(): Promise<void> {
|
|
const pool = getPool();
|
|
let success = 0;
|
|
let failed = 0;
|
|
|
|
for (const stmt of STATEMENTS) {
|
|
try {
|
|
await pool.query(stmt);
|
|
success++;
|
|
} catch (e: any) {
|
|
failed++;
|
|
logger.warn(`[ecrMigration] 실패 (계속 진행): ${e?.message?.slice(0, 200)}`);
|
|
}
|
|
}
|
|
|
|
logger.info(`🌱 ECR 마이그레이션 완료: 성공 ${success}건 / 실패 ${failed}건`);
|
|
}
|