diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index ba64f79..2c4c90a 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -30,3 +30,34 @@ jobs: - name: Build and Test with Gradle run: ./gradlew build working-directory: ${{ env.WORKING_DIRECTORY }} + + - name: Notify on Success (Discord) + if: success() + run: | + curl -X POST "${{ secrets.DISCORD_WEBHOOK_URL_CI }}" \ + -H "Content-Type: application/json" \ + -d '{ + "content": "[알림] 백엔드 CI 성공!", + "embeds": [{ + "title": "Backend CI Success!", + "description": "> [[PR #${{ github.event.number }}](${{ github.event.pull_request.html_url }})] ${{ github.event.pull_request.title }} (`${{ github.base_ref }}` ← `${{ github.head_ref }}`)", + "color": 65280 + }] + }' + + - name: Notify on Failure (Discord) + if: failure() + run: | + curl -X POST "${{ secrets.DISCORD_WEBHOOK_URL_CI }}" \ + -H "Content-Type: application/json" \ + -d '{ + "content": "[알림] 백엔드 CI 실패 - @everyone", + "embeds": [{ + "title": "Backend CI Failed", + "description": "> [[PR #${{ github.event.number }}](${{ github.event.pull_request.html_url }})] ${{ github.event.pull_request.title }} (`${{ github.base_ref }}` ← `${{ github.head_ref }}`)", + "color": 16711680, + "fields": [ + {"name": "🔗 Detail", "value": "[Action #${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})", "inline": false} + ] + }] + }' diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml index 37021f2..150fe30 100644 --- a/.github/workflows/deploy-main.yml +++ b/.github/workflows/deploy-main.yml @@ -66,6 +66,65 @@ jobs: labels: | org.opencontainers.image.source=https://github.com/${{ github.repository }} + - name: Notify on Success (Discord) + if: success() + env: + EVENT_NAME: ${{ github.event_name }} + PR_NUM: ${{ github.event.number }} + PR_URL: ${{ github.event.pull_request.html_url }} + PR_TITLE: ${{ github.event.pull_request.title }} + BASE_REF: ${{ github.base_ref }} + HEAD_REF: ${{ github.head_ref }} + SHA: ${{ github.sha }} + ACTOR: ${{ github.actor }} + run: | + if [ "$EVENT_NAME" = "pull_request" ]; then + DESCRIPTION="> [[PR #${PR_NUM}](${PR_URL})] ${PR_TITLE} (\`${BASE_REF}\` ← \`${HEAD_REF}\`)" + else + DESCRIPTION="> [\`${BASE_REF}\`] Deploy by ${ACTOR} (${SHA})" + fi + curl -X POST "${{ secrets.DISCORD_WEBHOOK_URL_CD }}" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg desc "$DESCRIPTION" '{ + "content": "[알림] 백엔드 빌드/배포 성공!", + "embeds": [{ + "title": "Backend Build & Deploy Success!", + "description": $desc, + "color": 65280 + }] + }')" + + - name: Notify on Failure (Discord) + if: failure() + env: + EVENT_NAME: ${{ github.event_name }} + PR_NUM: ${{ github.event.number }} + PR_URL: ${{ github.event.pull_request.html_url }} + PR_TITLE: ${{ github.event.pull_request.title }} + BASE_REF: ${{ github.base_ref }} + HEAD_REF: ${{ github.head_ref }} + SHA: ${{ github.sha }} + ACTOR: ${{ github.actor }} + run: | + if [ "$EVENT_NAME" = "pull_request" ]; then + DESCRIPTION="> [[PR #${PR_NUM}](${PR_URL})] ${PR_TITLE} (\`${BASE_REF}\` ← \`${HEAD_REF}\`)" + else + DESCRIPTION="> [\`${BASE_REF}\`] Deploy by ${ACTOR} (${SHA})" + fi + curl -X POST "${{ secrets.DISCORD_WEBHOOK_URL_CI }}" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg desc "$DESCRIPTION" '{ + "content": "[알림] 백엔드 빌드/배포 실패 - @everyone", + "embeds": [{ + "title": "Backend Build & Deploy Failed", + "description": $desc, + "color": 16711680, + "fields": [ + {"name": "🔗 Detail", "value": "[Action #${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})", "inline": false} + ] + }] + }')" + frontend: name: Frontend Build runs-on: ubuntu-latest @@ -91,6 +150,65 @@ jobs: - name: Build frontend run: npm run build + - name: Notify on Success (Discord) + if: success() + env: + EVENT_NAME: ${{ github.event_name }} + PR_NUM: ${{ github.event.number }} + PR_URL: ${{ github.event.pull_request.html_url }} + PR_TITLE: ${{ github.event.pull_request.title }} + BASE_REF: ${{ github.base_ref }} + HEAD_REF: ${{ github.head_ref }} + SHA: ${{ github.sha }} + ACTOR: ${{ github.actor }} + run: | + if [ "$EVENT_NAME" = "pull_request" ]; then + DESCRIPTION="> [[PR #${PR_NUM}](${PR_URL})] ${PR_TITLE} (\`${BASE_REF}\` ← \`${HEAD_REF}\`)" + else + DESCRIPTION="> [\`${BASE_REF}\`] Deploy by ${ACTOR} (${SHA})" + fi + curl -X POST "${{ secrets.DISCORD_WEBHOOK_URL_CI }}" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg desc "$DESCRIPTION" '{ + "content": "[알림] 프론트엔드 빌드 성공!", + "embeds": [{ + "title": "Frontend Build Success!", + "description": $desc, + "color": 65280 + }] + }')" + + - name: Notify on Failure (Discord) + if: failure() + env: + EVENT_NAME: ${{ github.event_name }} + PR_NUM: ${{ github.event.number }} + PR_URL: ${{ github.event.pull_request.html_url }} + PR_TITLE: ${{ github.event.pull_request.title }} + BASE_REF: ${{ github.base_ref }} + HEAD_REF: ${{ github.head_ref }} + SHA: ${{ github.sha }} + ACTOR: ${{ github.actor }} + run: | + if [ "$EVENT_NAME" = "pull_request" ]; then + DESCRIPTION="> [[PR #${PR_NUM}](${PR_URL})] ${PR_TITLE} (\`${BASE_REF}\` ← \`${HEAD_REF}\`)" + else + DESCRIPTION="> [\`${BASE_REF}\`] Deploy by ${ACTOR} (${SHA})" + fi + curl -X POST "${{ secrets.DISCORD_WEBHOOK_URL_CI }}" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg desc "$DESCRIPTION" '{ + "content": "[알림] 프론트엔드 빌드 실패 - @everyone", + "embeds": [{ + "title": "Frontend Build Failed", + "description": $desc, + "color": 16711680, + "fields": [ + {"name": "🔗 Detail", "value": "[Action #${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})", "inline": false} + ] + }] + }')" + deploy: name: Deploy to EC2 runs-on: ubuntu-latest diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml index 5fd0f1f..9e7b6a2 100644 --- a/.github/workflows/frontend-ci.yml +++ b/.github/workflows/frontend-ci.yml @@ -30,3 +30,34 @@ jobs: - name: Build frontend run: npm run build working-directory: ${{ env.WORKING_DIRECTORY }} + + - name: Notify on Success (Discord) + if: success() + run: | + curl -X POST "${{ secrets.DISCORD_WEBHOOK_URL_CI }}" \ + -H "Content-Type: application/json" \ + -d '{ + "content": "[알림] 프론트엔드 CI 성공!", + "embeds": [{ + "title": "Frontend CI Success!", + "description": "> [[PR #${{ github.event.number }}](${{ github.event.pull_request.html_url }})] ${{ github.event.pull_request.title }} (`${{ github.base_ref }}` ← `${{ github.head_ref }}`)", + "color": 65280 + }] + }' + + - name: Notify on Failure (Discord) + if: failure() + run: | + curl -X POST "${{ secrets.DISCORD_WEBHOOK_URL_CI }}" \ + -H "Content-Type: application/json" \ + -d '{ + "content": "[알림] 프론트엔드 CI 실패 - @everyone", + "embeds": [{ + "title": "Frontend CI Failed", + "description": "> [[PR #${{ github.event.number }}](${{ github.event.pull_request.html_url }})] ${{ github.event.pull_request.title }} (`${{ github.base_ref }}` ← `${{ github.head_ref }}`)", + "color": 16711680, + "fields": [ + {"name": "🔗 Detail", "value": "[Action #${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})", "inline": false} + ] + }] + }' diff --git a/agent/history/inley/feat-alert.md b/agent/history/inley/feat-alert.md new file mode 100644 index 0000000..8825e07 --- /dev/null +++ b/agent/history/inley/feat-alert.md @@ -0,0 +1,15 @@ +### Discord CI/CD 알림 추가 (Claude Code) +- **User Intent**: 3개 워크플로우(backend-ci, frontend-ci, deploy-main)에 빌드/배포 결과를 Discord로 전송하는 알림 기능을 추가 요청. 기존 프로젝트에서 사용하던 Discord embed 포맷을 그대로 재사용. +- **Agent Context**: 현재 모든 워크플로우에 Discord 알림이 전혀 없는 상태였음. 기존 코드는 전혀 수정하지 않고, 각 job의 마지막 step 뒤에 Notify step만 append 방식으로 추가. +- **Key Decisions**: + - PR 전용 CI(backend-ci, frontend-ci)는 간단한 curl+고정 포맷 사용 — 트리거가 PR로 고정되어 PR 컨텍스트 항상 사용 가능 + - PR+Push 혼용(deploy-main.yml)은 bash 조건문으로 PR/push 분기 처리 — `github.event_name`으로 PR 컨텍스트와 push 컨텍스트 각각 적절한 description 포맷 적용 + - Deploy job의 알림은 PR/push 구분 없이 단일 포맷 사용 — `github.ref_name`, `github.actor`, `github.sha`로 항상 식별 가능 +- **Affected Files**:
3개 파일 + + - **Modified**: + - `.github/workflows/backend-ci.yml` (+31/-0) — PR→dev 빌드 성공/실패 Discord 알림 step 추가 (success+failure) + - `.github/workflows/frontend-ci.yml` (+31/-0) — PR→dev 빌드 성공/실패 Discord 알림 step 추가 (success+failure) + - `.github/workflows/deploy-main.yml` (+118/-0) — backend/frontend/deploy 3개 job 각각 성공/실패 Discord 알림 추가, deploy-main은 PR/push 이벤트 모두 대응 + +
diff --git a/backend/src/main/resources/data.sql b/backend/src/main/resources/data.sql index a29e579..4950851 100644 --- a/backend/src/main/resources/data.sql +++ b/backend/src/main/resources/data.sql @@ -5,88 +5,400 @@ ALTER TABLE ai_query_history MODIFY COLUMN execution_status VARCHAR(50) NOT NULL; -- =================================================================== +-- 1. 회원 도메인 (USER / AUTH) -- =================================================================== --- 1. 회원 도메인 (MEMBER) --- =================================================================== -INSERT IGNORE INTO users (employee_no, user_name, department, password, role) VALUES -('EMP202601', '김관리', 'DevOps/AI 아키텍트 팀', '$2a$10$YnK23Eld...', 'ADMIN'), -('EMP202602', '김작업', 'MES/백엔드 개발 팀', '$2a$10$Zk91Mdls...', 'MANAGER'), -('EMP202603', '이창고', '물류 관리 팀 (WMS)', '$2a$10$Ab12Cd34...', 'WORKER'), -('EMP202604', '박공정', '생산 공정 A라인', '$2a$10$Ef56Gh78...', 'WORKER'); +INSERT IGNORE INTO users (employee_no, user_name, department, password, role, created_at, updated_at) +WITH RECURSIVE seq(n) AS ( + SELECT 1 + UNION ALL + SELECT n + 1 FROM seq WHERE n < 50 +) +SELECT + CASE WHEN n = 1 THEN 'user001' ELSE CONCAT('user', LPAD(n, 3, '0')) END, + CASE WHEN n = 1 THEN '관리자' ELSE CONCAT('사용자', LPAD(n, 3, '0')) END, + ELT(1 + MOD(n - 1, 5), '생산관리팀', '자재관리팀', '품질관리팀', '물류관리팀', '설비관리팀'), + CASE WHEN n = 1 THEN '$2y$10$yRO8UxkCZtLXfB9xpHHrdeKwsNfOFiMaC0kfHWgkZeQFNr6BeWenK' ELSE '$2y$10$yRO8UxkCZtLXfB9xpHHrdeKwsNfOFiMaC0kfHWgkZeQFNr6BeWenK' END, + CASE WHEN n = 1 THEN 'ADMIN' WHEN MOD(n, 5) = 0 THEN 'MANAGER' ELSE 'WORKER' END, + TIMESTAMP('2026-01-01 08:00:00') + INTERVAL n DAY, + TIMESTAMP('2026-01-01 08:00:00') + INTERVAL n DAY +FROM seq; +INSERT IGNORE INTO refresh_tokens (user_id, token_hash, expires_at, revoked, created_at, updated_at) +WITH RECURSIVE seq(n) AS ( + SELECT 1 + UNION ALL + SELECT n + 1 FROM seq WHERE n < 50 +) +SELECT + u.user_id, + SHA2(CONCAT('refresh-token-', LPAD(n, 3, '0')), 256), + TIMESTAMP('2026-06-24 23:59:59') + INTERVAL n DAY, + CASE WHEN MOD(n, 10) = 0 THEN 1 ELSE 0 END, + TIMESTAMP('2026-01-02 09:00:00') + INTERVAL n DAY, + TIMESTAMP('2026-01-02 09:00:00') + INTERVAL n DAY +FROM seq +JOIN users u ON u.employee_no = CASE WHEN n = 1 THEN 'user001' ELSE CONCAT('user', LPAD(n, 3, '0')) END; -- =================================================================== --- 2. 기준정보 관리 도메인 (MASTER DATA) +-- 2. 기준정보 도메인 (MASTER DATA) -- =================================================================== +INSERT IGNORE INTO item_master (item_code, item_name, spec, unit, item_type, safety_stock, item_status, created_at) +WITH RECURSIVE seq(n) AS ( + SELECT 1 + UNION ALL + SELECT n + 1 FROM seq WHERE n < 50 +) +SELECT + CASE + WHEN n <= 20 THEN CONCAT('RM-', LPAD(n, 3, '0')) + WHEN n <= 35 THEN CONCAT('SM-', LPAD(n - 20, 3, '0')) + ELSE CONCAT('FG-', LPAD(n - 35, 3, '0')) + END, + CASE + WHEN n <= 20 THEN CONCAT('원자재 ', LPAD(n, 3, '0')) + WHEN n <= 35 THEN CONCAT('반제품 ', LPAD(n - 20, 3, '0')) + ELSE CONCAT('완제품 ', LPAD(n - 35, 3, '0')) + END, + CONCAT(ELT(1 + MOD(n - 1, 5), 'STD', 'PRO', 'ECO', 'HEAVY', 'LIGHT'), '-', LPAD(n, 3, '0')), + ELT(1 + MOD(n - 1, 4), 'ea', 'kg', 'box', 'L'), + CASE WHEN n <= 20 THEN 'RAW' WHEN n <= 35 THEN 'HALF' ELSE 'FG' END, + 50 + (n * 10), + CASE WHEN MOD(n, 17) = 0 THEN 'INACTIVE' ELSE 'ACTIVE' END, + TIMESTAMP('2026-01-03 08:00:00') + INTERVAL n DAY +FROM seq; --- [item_master] 인서트 -INSERT IGNORE INTO item_master (item_code, item_name, spec, unit, item_type, safety_stock) VALUES -('RM-STEEL-01', '고탄소 탄소강판', '2.0T * 1219 * 2438', 'kg', 'RAW', 5000), -('RM-CHIP-5G', '통신 제어용 메인 칩셋', 'Snapdragon IoT-v3', 'ea', 'RAW', 200), -('RM-CABLE-02', '고온 절연 와이어 케이블', 'Ø5.0 / 100m roll', 'box', 'RAW', 50), -('RM-PLASTIC-P', '강화 플라스틱 사출 케이스', 'Standard-450', 'ea', 'RAW', 1000), -('SM-PCB-ASSY', '제어보드 PCB 조립체', 'SMD-DoubleSided', 'ea', 'HALF', 300), -('SM-FRAME-A', '강판 프레임 용접 모듈', 'Standard-A급 용접형', 'ea', 'HALF', 150), -('FP-SMART-BOX', '스마트 물류 제어 단말기', 'SB-2026-Pro', 'ea', 'FG', 100), -('FP-CONTROLLER', '산업용 모터 인버터 정밀 제어기', 'IC-100HP', 'ea', 'FG', 50); - --- 생성된 AUTO_INCREMENT PK 값을 안전하게 추적하기 위해 변수에 담아둡니다. -SET @rm_steel = (SELECT item_id FROM item_master WHERE item_code = 'RM-STEEL-01'); -SET @rm_chip = (SELECT item_id FROM item_master WHERE item_code = 'RM-CHIP-5G'); -SET @rm_cable = (SELECT item_id FROM item_master WHERE item_code = 'RM-CABLE-02'); -SET @rm_case = (SELECT item_id FROM item_master WHERE item_code = 'RM-PLASTIC-P'); -SET @sm_pcb = (SELECT item_id FROM item_master WHERE item_code = 'SM-PCB-ASSY'); -SET @sm_frame = (SELECT item_id FROM item_master WHERE item_code = 'SM-FRAME-A'); -SET @fp_box = (SELECT item_id FROM item_master WHERE item_code = 'FP-SMART-BOX'); -SET @fp_ctrl = (SELECT item_id FROM item_master WHERE item_code = 'FP-CONTROLLER'); +INSERT IGNORE INTO partner_master ( + partner_code, partner_name, partner_type, business_no, representative, + contact_phone, contact_email, note, partner_status, created_at +) +WITH RECURSIVE seq(n) AS ( + SELECT 1 + UNION ALL + SELECT n + 1 FROM seq WHERE n < 50 +) +SELECT + CASE WHEN n <= 25 THEN CONCAT('SUP-', LPAD(n, 3, '0')) ELSE CONCAT('CUS-', LPAD(n - 25, 3, '0')) END, + CASE WHEN n <= 25 THEN CONCAT('공급사 ', LPAD(n, 3, '0')) ELSE CONCAT('고객사 ', LPAD(n - 25, 3, '0')) END, + CASE WHEN n <= 25 THEN 'SUPPLIER' ELSE 'CUSTOMER' END, + CONCAT('2026-', LPAD(n, 2, '0'), '-', LPAD(10000 + n, 5, '0')), + CONCAT('대표자', LPAD(n, 3, '0')), + CONCAT('02-26', LPAD(n, 2, '0'), '-', LPAD(1000 + n, 4, '0')), + CONCAT('partner', LPAD(n, 3, '0'), '@example.com'), + CONCAT('2026년 더미 거래처 ', LPAD(n, 3, '0')), + CASE WHEN MOD(n, 19) = 0 THEN 'INACTIVE' ELSE 'ACTIVE' END, + TIMESTAMP('2026-01-04 08:00:00') + INTERVAL n DAY +FROM seq; +INSERT IGNORE INTO warehouse_location ( + location_code, warehouse_name, max_capacity, rack_row, rack_column, production_receipt_default +) +WITH RECURSIVE seq(n) AS ( + SELECT 1 + UNION ALL + SELECT n + 1 FROM seq WHERE n < 50 +) +SELECT + CONCAT('WH', LPAD(1 + MOD(n - 1, 5), 2, '0'), '-R', CHAR(65 + MOD(n - 1, 10)), '-C', LPAD(1 + FLOOR((n - 1) / 10), 2, '0')), + ELT(1 + MOD(n - 1, 5), '원자재 창고', '반제품 창고', '완제품 창고', '검사 대기 창고', '출하 대기 창고'), + 1000 + (n * 100), + CHAR(65 + MOD(n - 1, 10)), + LPAD(1 + FLOOR((n - 1) / 10), 2, '0'), + CASE WHEN MOD(n, 10) = 0 THEN 1 ELSE 0 END +FROM seq; --- [partner_master] 인서트 -INSERT IGNORE INTO partner_master (partner_code, partner_name, partner_type, business_no, representative, contact_phone) VALUES -('SUP-POSCO-01', '(주)포스코 인터내셔널', 'SUPPLIER', '123-45-67890', '이구택', '02-3457-1114'), -('SUP-SAMSUNG-E', '삼성전자 디바이스솔루션', 'SUPPLIER', '220-81-62517', '경계현', '031-200-1114'), -('SUP-SAMWHA-0', '삼화전기(주)', 'SUPPLIER', '301-81-04281', '오영주', '043-261-0111'), -('CUS-HYUNDAI-M', '현대모비스 울산공장', 'CUSTOMER', '113-81-22441', '이규석', '052-202-0114'), -('CUS-LG-ENSOL', 'LG에너지솔루션 오창공장', 'CUSTOMER', '582-88-01230', '권영수', '043-219-0114'); +INSERT IGNORE INTO factory_routing (factory_name, line_name, operation_seq, operation_name, routing_status, created_at) +WITH RECURSIVE seq(n) AS ( + SELECT 1 + UNION ALL + SELECT n + 1 FROM seq WHERE n < 50 +) +SELECT + CONCAT('창원제', 1 + FLOOR((n - 1) / 25), '공장'), + CONCAT(CHAR(65 + MOD(FLOOR((n - 1) / 5), 5)), '라인'), + 1 + MOD(n - 1, 5), + ELT(1 + MOD(n - 1, 5), '자재 투입', '가공', '조립', '검사', '포장'), + CASE WHEN MOD(n, 23) = 0 THEN 'INACTIVE' ELSE 'ACTIVE' END, + TIMESTAMP('2026-01-05 08:00:00') + INTERVAL n DAY +FROM seq; +-- =================================================================== +-- 3. BOM 도메인 +-- =================================================================== +INSERT IGNORE INTO bom_structure (parent_item_id, child_item_id, quantity, bom_version, bom_status, created_at) +WITH RECURSIVE seq(n) AS ( + SELECT 1 + UNION ALL + SELECT n + 1 FROM seq WHERE n < 50 +) +SELECT + parent_item.item_id, + child_item.item_id, + 1 + MOD(n, 8), + 'v1.0', + CASE WHEN MOD(n, 29) = 0 THEN 'INACTIVE' ELSE 'ACTIVE' END, + TIMESTAMP('2026-01-06 08:00:00') + INTERVAL n DAY +FROM seq +JOIN item_master parent_item + ON parent_item.item_code = CASE + WHEN n <= 30 THEN CONCAT('SM-', LPAD(1 + MOD(n - 1, 15), 3, '0')) + ELSE CONCAT('FG-', LPAD(1 + MOD(n - 31, 15), 3, '0')) + END +JOIN item_master child_item + ON child_item.item_code = CASE + WHEN n <= 30 THEN CONCAT('RM-', LPAD(1 + MOD((n * 7) - 1, 20), 3, '0')) + ELSE CONCAT('SM-', LPAD(1 + MOD((n - 31) + (5 * FLOOR((n - 31) / 15)), 15), 3, '0')) + END; --- [factory_routing] 인서트 -INSERT IGNORE INTO factory_routing (factory_name, line_name, operation_seq, operation_name) VALUES -('창원제1공장', 'A라인', 1, 'SMD 표면실장 공정'), -('창원제1공장', 'A라인', 2, '프레임 기계조립 공정'), -('창원제1공장', 'A라인', 3, '펌웨어 인젝션 및 최종 검사 공정'), -('창원제1공장', 'B라인', 1, '메인보드 배선 공정'), -('창원제1공장', 'B라인', 2, '인버터 하우징 조립 공정'), -('창원제1공장', 'B라인', 3, '에이징 테스트 및 포장 공정'); +-- =================================================================== +-- 4. 입고 및 재고 도메인 (WMS / INVENTORY) +-- =================================================================== +INSERT IGNORE INTO current_inventory (item_id, location_id, lot_number, current_qty, first_inbound_date, updated_at) +WITH RECURSIVE seq(n) AS ( + SELECT 1 + UNION ALL + SELECT n + 1 FROM seq WHERE n < 50 +) +SELECT + item.item_id, + location.location_id, + CONCAT('LOT-', LPAD(n, 3, '0')), + 100 + (n * 13), + DATE('2026-01-01') + INTERVAL n DAY, + TIMESTAMP('2026-02-01 10:00:00') + INTERVAL n DAY +FROM seq +JOIN item_master item + ON item.item_code = CASE + WHEN n <= 20 THEN CONCAT('RM-', LPAD(n, 3, '0')) + WHEN n <= 35 THEN CONCAT('SM-', LPAD(n - 20, 3, '0')) + ELSE CONCAT('FG-', LPAD(n - 35, 3, '0')) + END +JOIN warehouse_location location + ON location.location_code = CONCAT('WH', LPAD(1 + MOD(n - 1, 5), 2, '0'), '-R', CHAR(65 + MOD(n - 1, 10)), '-C', LPAD(1 + FLOOR((n - 1) / 10), 2, '0')); +INSERT IGNORE INTO inbound_receipt (item_id, partner_id, location_id, inbound_qty, inbound_date, worker_id, status, created_at) +WITH RECURSIVE seq(n) AS ( + SELECT 1 + UNION ALL + SELECT n + 1 FROM seq WHERE n < 50 +) +SELECT + item.item_id, + partner.partner_id, + location.location_id, + 50 + (n * 5), + DATE('2026-01-10') + INTERVAL n DAY, + worker.user_id, + ELT(1 + MOD(n - 1, 3), 'READY', 'COMPLETED', 'STACKED'), + TIMESTAMP('2026-01-10 09:30:00') + INTERVAL n DAY +FROM seq +JOIN item_master item + ON item.item_code = CASE + WHEN n <= 20 THEN CONCAT('RM-', LPAD(n, 3, '0')) + WHEN n <= 35 THEN CONCAT('SM-', LPAD(n - 20, 3, '0')) + ELSE CONCAT('FG-', LPAD(n - 35, 3, '0')) + END +JOIN partner_master partner + ON partner.partner_code = CONCAT('SUP-', LPAD(1 + MOD(n - 1, 25), 3, '0')) +JOIN warehouse_location location + ON location.location_code = CONCAT('WH', LPAD(1 + MOD(n - 1, 5), 2, '0'), '-R', CHAR(65 + MOD(n - 1, 10)), '-C', LPAD(1 + FLOOR((n - 1) / 10), 2, '0')) +JOIN users worker + ON worker.employee_no = CONCAT('user', LPAD(1 + MOD(n, 50), 3, '0')); --- [bom_structure] 인서트 (추출한 대리 키 변수를 바인딩) -INSERT IGNORE INTO bom_structure (parent_item_id, child_item_id, quantity, bom_version) VALUES --- 1. 반제품 '제어보드 PCB 조립체'를 만들기 위해: 메인 칩셋 1개, 케이블 0.05박스 투입 -(@sm_pcb, @rm_chip, 1.0000, 'v1.0'), -(@sm_pcb, @rm_cable, 0.0500, 'v1.0'), +-- =================================================================== +-- 5. 생산 도메인 +-- =================================================================== +INSERT IGNORE INTO work_order (order_no, item_id, routing_id, target_qty, bom_version, status, plan_date, created_at, updated_at) +WITH RECURSIVE seq(n) AS ( + SELECT 1 + UNION ALL + SELECT n + 1 FROM seq WHERE n < 50 +) +SELECT + CONCAT('WO-2026-', LPAD(n, 4, '0')), + item.item_id, + routing.routing_id, + 100 + (n * 4), + 'v1.0', + ELT(1 + MOD(n - 1, 4), 'READY', 'RUN', 'HOLD', 'CLOSE'), + DATE('2026-02-01') + INTERVAL n DAY, + TIMESTAMP('2026-01-20 08:00:00') + INTERVAL n DAY, + TIMESTAMP('2026-01-20 08:00:00') + INTERVAL n DAY +FROM seq +JOIN item_master item + ON item.item_code = CASE WHEN n <= 25 THEN CONCAT('SM-', LPAD(1 + MOD(n - 1, 15), 3, '0')) ELSE CONCAT('FG-', LPAD(1 + MOD(n - 26, 15), 3, '0')) END +JOIN factory_routing routing + ON routing.factory_name = CONCAT('창원제', 1 + FLOOR((n - 1) / 25), '공장') + AND routing.line_name = CONCAT(CHAR(65 + MOD(FLOOR((n - 1) / 5), 5)), '라인') + AND routing.operation_seq = 1 + MOD(n - 1, 5); --- 2. 반제품 '강판 프레임 용접 모듈'을 만들기 위해: 탄소강판 12.5kg 투입 -(@sm_frame, @rm_steel, 12.5000, 'v1.0'), +INSERT IGNORE INTO work_order_sequence (plan_date, last_sequence) +WITH RECURSIVE seq(n) AS ( + SELECT 1 + UNION ALL + SELECT n + 1 FROM seq WHERE n < 50 +) +SELECT + DATE('2026-02-01') + INTERVAL n DAY, + 1 + MOD(n, 9) +FROM seq; --- 3. 완제품 '스마트 물류 제어 단말기'를 만들기 위해: PCB조립체 1개, 프레임모듈 1개, 사출케이스 1개 투입 -(@fp_box, @sm_pcb, 1.0000, 'v1.0'), -(@fp_box, @sm_frame, 1.0000, 'v1.0'), -(@fp_box, @rm_case, 1.0000, 'v1.0'), +INSERT IGNORE INTO production_execution ( + order_id, routing_id, good_qty, defect_qty, defect_type, defect_reason, reworkable, + worker_id, man_hours_minutes, created_at +) +WITH RECURSIVE seq(n) AS ( + SELECT 1 + UNION ALL + SELECT n + 1 FROM seq WHERE n < 50 +) +SELECT + work_order.order_id, + routing.routing_id, + 80 + (n * 3), + MOD(n, 6), + CASE WHEN MOD(n, 6) = 0 THEN NULL ELSE ELT(1 + MOD(n - 1, 4), '외관 불량', '치수 불량', '전장 불량', '포장 불량') END, + CASE WHEN MOD(n, 6) = 0 THEN NULL ELSE CONCAT('검사 기준 미달 ', LPAD(n, 3, '0')) END, + CASE WHEN MOD(n, 2) = 0 THEN 1 ELSE 0 END, + worker.user_id, + 60 + (n * 7), + TIMESTAMP('2026-02-01 13:00:00') + INTERVAL n DAY +FROM seq +JOIN work_order ON work_order.order_no = CONCAT('WO-2026-', LPAD(n, 4, '0')) +JOIN factory_routing routing + ON routing.factory_name = CONCAT('창원제', 1 + FLOOR((n - 1) / 25), '공장') + AND routing.line_name = CONCAT(CHAR(65 + MOD(FLOOR((n - 1) / 5), 5)), '라인') + AND routing.operation_seq = 1 + MOD(n - 1, 5) +JOIN users worker + ON worker.employee_no = CONCAT('user', LPAD(1 + MOD(n + 1, 50), 3, '0')); --- 4. 완제품 '산업용 모터 인버터 정밀 제어기'를 만들기 위해: 메인 칩셋 2개, 케이블 0.2박스 투입 -(@fp_ctrl, @rm_chip, 2.0000, 'v1.0'), -(@fp_ctrl, @rm_cable, 0.2000, 'v1.0'); +INSERT IGNORE INTO inventory_transaction_history ( + item_id, location_id, transaction_type, quantity, reason_desc, + work_order_id, production_execution_id, original_transaction_id, worker_id, created_at +) +WITH RECURSIVE seq(n) AS ( + SELECT 1 + UNION ALL + SELECT n + 1 FROM seq WHERE n < 50 +) +SELECT + item.item_id, + location.location_id, + ELT(1 + MOD(n - 1, 12), 'RESERVATION', 'INBOUND', 'OUTBOUND', 'PRODUCTION_ISSUE', 'PRODUCTION_ISSUE_CANCEL', 'PRODUCTION_RECEIPT', 'PRODUCTION_RECEIPT_CANCEL', 'ADJUSTMENT', 'TRANSFER_OUT', 'TRANSFER_IN', 'SCRAP', 'RETURN'), + 10 + (n * 2), + CONCAT('더미 수불 이력 ', LPAD(n, 3, '0')), + CASE WHEN MOD(n, 3) = 0 THEN work_order.order_id ELSE NULL END, + CASE WHEN MOD(n, 4) = 0 THEN execution.execution_id ELSE NULL END, + NULL, + worker.user_id, + TIMESTAMP('2026-02-10 11:00:00') + INTERVAL n DAY +FROM seq +JOIN item_master item + ON item.item_code = CASE + WHEN n <= 20 THEN CONCAT('RM-', LPAD(n, 3, '0')) + WHEN n <= 35 THEN CONCAT('SM-', LPAD(n - 20, 3, '0')) + ELSE CONCAT('FG-', LPAD(n - 35, 3, '0')) + END +JOIN warehouse_location location + ON location.location_code = CONCAT('WH', LPAD(1 + MOD(n - 1, 5), 2, '0'), '-R', CHAR(65 + MOD(n - 1, 10)), '-C', LPAD(1 + FLOOR((n - 1) / 10), 2, '0')) +LEFT JOIN work_order ON work_order.order_no = CONCAT('WO-2026-', LPAD(n, 4, '0')) +LEFT JOIN production_execution execution ON execution.order_id = work_order.order_id +JOIN users worker + ON worker.employee_no = CONCAT('user', LPAD(1 + MOD(n + 2, 50), 3, '0')); +-- =================================================================== +-- 6. 출하 도메인 +-- =================================================================== +INSERT IGNORE INTO outbound_shipping ( + shipping_no, partner_id, item_id, request_qty, shipped_qty, shipping_type, + picking_location_id, vehicle_no, carrier, tracking_no, estimated_delivery, + cancel_reason, status, worker_id, shipped_at, created_at +) +WITH RECURSIVE seq(n) AS ( + SELECT 1 + UNION ALL + SELECT n + 1 FROM seq WHERE n < 50 +) +SELECT + CONCAT('SHP-2026-', LPAD(n, 4, '0')), + partner.partner_id, + item.item_id, + 20 + (n * 3), + CASE WHEN MOD(n, 7) = 0 THEN NULL ELSE 15 + (n * 3) END, + ELT(1 + MOD(n - 1, 3), 'SALE', 'TRANSFER', 'RETURN'), + location.location_id, + CONCAT('서울', LPAD(n, 2, '0'), '가', LPAD(1000 + n, 4, '0')), + ELT(1 + MOD(n - 1, 4), 'CJ대한통운', '한진택배', '롯데글로벌로지스', '직배송'), + CONCAT('TRK2026', LPAD(n, 6, '0')), + DATE('2026-03-01') + INTERVAL n DAY, + CASE WHEN MOD(n, 13) = 0 THEN '고객 요청 취소' ELSE NULL END, + ELT(1 + MOD(n - 1, 7), 'READY', 'PICKING', 'PACKING', 'INSPECTING', 'SHIPPED', 'PARTIALLY_SHIPPED', 'CANCELED'), + worker.user_id, + CASE WHEN MOD(n, 7) IN (5, 6) THEN TIMESTAMP('2026-03-01 15:00:00') + INTERVAL n DAY ELSE NULL END, + TIMESTAMP('2026-02-20 09:00:00') + INTERVAL n DAY +FROM seq +JOIN partner_master partner + ON partner.partner_code = CONCAT('CUS-', LPAD(1 + MOD(n - 1, 25), 3, '0')) +JOIN item_master item + ON item.item_code = CONCAT('FG-', LPAD(1 + MOD(n - 1, 15), 3, '0')) +JOIN warehouse_location location + ON location.location_code = CONCAT('WH', LPAD(1 + MOD(n - 1, 5), 2, '0'), '-R', CHAR(65 + MOD(n - 1, 10)), '-C', LPAD(1 + FLOOR((n - 1) / 10), 2, '0')) +JOIN users worker + ON worker.employee_no = CONCAT('user', LPAD(1 + MOD(n + 3, 50), 3, '0')); -- =================================================================== --- 3. 입고 및 재고 관리 도메인 (WMS / INVENTORY) +-- 7. AI 도메인 -- =================================================================== +INSERT IGNORE INTO ai_query_history ( + worker_id, natural_question, conversation_id, parent_query_id, effective_question, + generated_sql, natural_answer, result_json, chart_spec_json, row_count, + execution_time_ms, model_name, execution_status, error_log, created_at +) +WITH RECURSIVE seq(n) AS ( + SELECT 1 + UNION ALL + SELECT n + 1 FROM seq WHERE n < 50 +) +SELECT + worker.user_id, + CONCAT('2026년 재고와 생산 현황을 조회해줘 ', LPAD(n, 3, '0')), + CONCAT('conv-2026-', LPAD(1 + FLOOR((n - 1) / 5), 3, '0')), + NULL, + CONCAT('2026년 재고와 생산 현황 조회 ', LPAD(n, 3, '0')), + 'SELECT COUNT(*) FROM current_inventory', + CONCAT('조회 결과 ', 10 + n, '건입니다.'), + CONCAT('{"rows":', 10 + n, '}'), + CONCAT('{"type":"bar","index":', n, '}'), + 10 + n, + 1000 + (n * 37), + 'gpt-4.1-mini', + ELT(1 + MOD(n - 1, 6), 'SUCCESS', 'CLARIFICATION_REQUIRED', 'CLARIFICATION_ANSWERED', 'SQL_EXECUTION_FAILED', 'ANSWER_GENERATION_FAILED', 'TIMEOUT'), + CASE WHEN MOD(n, 6) IN (3, 4, 5) THEN CONCAT('더미 오류 로그 ', LPAD(n, 3, '0')) ELSE NULL END, + TIMESTAMP('2026-03-10 10:00:00') + INTERVAL n DAY +FROM seq +JOIN users worker + ON worker.employee_no = CONCAT('user', LPAD(1 + MOD(n + 4, 50), 3, '0')); --- [warehouse_location] 인서트 -INSERT IGNORE INTO warehouse_location (location_code, warehouse_name, rack_row, rack_column, production_receipt_default) VALUES -('WH01-RACK-A1', '원자재 창고', 'A열', '1단', false), -('WH01-RACK-A2', '원자재 창고', 'A열', '2단', false), -('WH02-RACK-B1', '반제품 창고', 'B열', '1단', false), -('WH03-RACK-C1', '완제품 창고', 'C열', '1단', true), -('WH03-RACK-C2', '완제품 창고', 'C열', '2단', false); +INSERT IGNORE INTO dynamic_batch_schedule ( + schedule_name, cron_expression, query_id, is_active, last_run_at, + last_run_status, worker_id, created_at +) +WITH RECURSIVE seq(n) AS ( + SELECT 1 + UNION ALL + SELECT n + 1 FROM seq WHERE n < 50 +) +SELECT + CONCAT('AI 정기 리포트 ', LPAD(n, 3, '0')), + ELT(1 + MOD(n - 1, 5), '0 0 8 * * *', '0 0 12 * * *', '0 0 18 * * *', '0 30 9 * * MON', '0 0 6 1 * *'), + query_history.query_id, + CASE WHEN MOD(n, 11) = 0 THEN 0 ELSE 1 END, + TIMESTAMP('2026-04-01 08:00:00') + INTERVAL n DAY, + ELT(1 + MOD(n - 1, 3), 'SUCCESS', 'FAILED', 'NONE'), + worker.user_id, + TIMESTAMP('2026-03-20 08:00:00') + INTERVAL n DAY +FROM seq +JOIN ai_query_history query_history + ON query_history.conversation_id = CONCAT('conv-2026-', LPAD(1 + FLOOR((n - 1) / 5), 3, '0')) + AND query_history.natural_question = CONCAT('2026년 재고와 생산 현황을 조회해줘 ', LPAD(n, 3, '0')) +JOIN users worker + ON worker.employee_no = CONCAT('user', LPAD(1 + MOD(n + 5, 50), 3, '0'));