278 Commits

Author SHA1 Message Date
chpark ed746e71a2 fix(inventory/history): 매입 입고 음수 차감은 '매입출고' 로 라벨
ref_type='PROCUREMENT' + move_type='OUT' 케이스(매입 입고 화면에서 음수 수량으로
재고 차감한 경우) 의 REF_TYPE_LABEL 을 '매입발주' → '매입출고' 로 분기.

기존 '매입발주' 라벨은 ref_type='PROCUREMENT' + move_type='IN' 일 때만 적용.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 00:53:57 +09:00
chpark 36edafcf16 ui(orders/new): '한정 잔여' → '출고 가능 N개' 배지로 강조
- 카드/리스트 모두: 작은 회색 텍스트 → 에메랄드 배경 테두리 배지로 변경
- 표현: '한정 잔여 N / M' → '출고 가능 N(단위)  / 한정 M'
- 잔여 0 이면 로즈 배경으로 시각 차별
- limit_qty 없는 품목은 표시 안 함 (무제한, 기존 동작 유지)

계산식 그대로: limit_qty - reserved_qty (사이클 누적 합산)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 00:38:13 +09:00
chpark adcc7e3b48 feat(orders): USER 마감 후 수정 차단 + ADMIN 한정수량 우회
USER (거래처):
- 자신의 발주 라인에 포함된 품목의 sale_end_date 가 지나면 수정/삭제 차단
  · orders/items/update: lineRes 에 is_closed 계산해서 마감 후 라인은 ROLLBACK
  · orders/items/add: 추가하려는 품목 중 하나라도 마감이면 ROLLBACK
  · 마감 판정은 strict less than (마감 시각 정각부터 차단), 자정 정각은 그날 종일
- 사용자 주문 상세: 마감된 ITEM 라인은 수량/삭제 버튼 비활성 + '마감' 배지,
       마감 라인이 있을 때 상단에 안내 한 줄

ADMIN (출고 담당자):
- 한정 수량(limit_qty) 검증 우회 — orders/save / items/add / items/update 모두
  isAdmin 일 때 한정 검증 블록 skip
- 마감 후에도 라인 수정 가능 (USER 만 차단되는 is_closed 가드 조건에 !isAdmin 포함)
- 1회 발주 한도(max_order_qty) 우회는 이전부터 적용

API: orders/detail 응답 라인에 SALE_END_DATE / IS_CLOSED 추가 (사용자 화면 가드용)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 00:35:59 +09:00
chpark 2afb5fecdd fix(cycle): 사이클 종료를 strict less than 으로 — 마감 시각 정각부터는 다음 사이클
이전: O.regdate <= sale_end_date (마감 시각 정각도 이번 사이클로 포함)
변경: O.regdate < sale_end_date (마감 시각 직전까지 = 17:59:59 까지)

예) 월요일 18:00 마감:
  · 17:59:59 발주 → 이번 사이클 (이번주 월요일 마감) 합산
  · 18:00:00 발주 → 다음 사이클 (다음주 월요일 마감 또는 화요일 마감)

자정 정각(00:00) 입력은 '그날 종일 마감' 으로 해석해 다음날 00:00 직전까지 포함.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 00:27:00 +09:00
chpark 4f2543686a feat(items): 한정 수량(limit_qty) + 출고일 배지 — 마감 사이클 단위 누적 상한
신규 컬럼: momo_items.limit_qty INTEGER (null/0 = 제한 없음)
  · ensureColumns 에서 ADD COLUMN IF NOT EXISTS 자동 보장
  · 관리자 품목 폼: '한정 수량 (이번 마감 사이클 누적 상한)' 필드 추가

마감 사이클 정의 (sale_end_date 요일 기준):
  · 월요일 마감(DOW=1): 저번주 금~월 마감 시각 (마감일 -3일 00:00 ~ 마감일)
  · 화요일 마감(DOW=2): 저번주 금~화 마감 시각 (마감일 -4일 00:00 ~ 마감일)
  · 그 외: sale_start_date ~ sale_end_date (fallback)

누적 합산 대상: 같은 사이클 안에 등록된 momo_order_items.qty
  · status: REQUESTED/APPROVED/PAID/INVOICED (CANCELLED 제외)
  · kind='ITEM' (택배/용차/환불 제외)

검증 (모든 사용자/관리자/unlimited_qty 권한 무관 적용):
  · orders/save: 이번 요청 합 + reserved <= limit_qty 체크
  · orders/items/add: 동일 검증 (트랜잭션 client 사용)
  · orders/items/update: newQty - oldQty 차이만 비교 (수량 증가 시)

신규 파일:
  · src/lib/momo-cycle.ts — getReservedQty(itemObjid, client?) 헬퍼

사용자 출고요청 화면(/m/orders/new):
  · 카드/리스트에 RESERVED_QTY 받아 '한정 잔여 N / 한정 M' 표시
  · 한정 소진 시 '한정 수량 소진' 배지 + 담기 버튼 비활성
  · 이미지 위 출고일 배지: 월요일 마감 → '수요일 출고', 화요일 마감 → '금요일 출고'
  · limit_qty 가 없는 품목은 무한대 (기존 동작 유지)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 00:17:20 +09:00
chpark 77f2ef2cd5 feat(orders): 모든 품목 재고 무관 출고요청 — 음수 재고는 매입/입고에서 발주 트리거
핵심 정책 변경:
- 기존: 택배전용 품목만 재고 무관 출고요청 가능 / 일반 품목은 재고 ≥ 요청 강제
- 신규: 모든 품목 재고 무관 출고요청 가능. 권한 체크는 sale_start/end_date,
        is_hidden(+view_hidden), max_order_qty(+unlimited_qty) 만 적용

API (재고 체크 제거 — 한도/숨김/판매기간만 유지):
- orders/save: ITEM 재고 초과 검증 제거. needsDelivery 자동 추가는 유지
- orders/items/add: 재고 초과 검증 제거
- orders/items/update: 재고 초과 검증 + stock_qty 조회 자체 제거
- items/list: onlyAvailable 재고 필터 제거(옵션은 호환 위해 no-op로 유지)

사용자 화면 — 재고 표시/품절 제거 (재고 없어도 출고 가능):
- /m/orders/new: 카드/리스트에서 STOCK_QTY 컬럼 + '품절' 배지 제거.
       한도 체크는 MAX_ORDER_QTY(권한자 무제한) 만 적용
- /m/orders 주문 상세 ItemPickerModal: 재고 컬럼 + max=stock 제거,
       stockFilter:'AVAILABLE' → forSale:true 로 교체

관리자 화면 — 현재고 표시 유지하되 음수 강조:
- /m/admin/orders 거래명세표: 현재고 음수면 bg-rose-50 + extrabold,
       '재고 부족' 경고를 '음수 재고가 됩니다' 안내로 톤 변경
- /m/admin/inventory(매입/입고): 재고 매트릭스 음수 셀 bg-rose-50 + extrabold

(approve API의 음수 재고 허용 정책은 이전부터 적용되어 있어 변경 없음)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 22:53:17 +09:00
chpark d6f80c187b feat(inventory): 매입 입고에서 음수 수량 허용 — 재고 차감 가능
- 입력 input의 min={1} 제거, 0만 막고 음수는 허용
- 입고 라인 표시: 양수 +N(에메랄드) / 음수 -N(로즈) 색상 구분
- API: ln.qty<=0 → ln.qty===0 만 skip. move_type 을 qty 부호로 IN/OUT 분기
       (qty 컬럼은 부호 그대로 — 기존 stock_moves 컨벤션 일치)
- 음수 라인은 cost_price 미갱신

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 22:20:04 +09:00
chpark c69e811f46 feat(m/orders/new): 30초 폴링 + 탭 포커스 복귀 시 자동 재조회
- 관리자가 품목을 숨김/노출/재고 변경해도 다른 사용자가 로그아웃/새로고침 없이 자동 반영
- silent 모드 추가해 백그라운드 재조회 시 '불러오는 중' 깜빡임 제거
- visibilitychange + focus 이벤트로 탭 복귀 즉시 최신화

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 22:11:07 +09:00
chpark 83ac3a5456 feat(daily-order-inventory): 단일 일자 → 기간(시작~종료) 조회 지원
- 페이지: date 두 개(시작/종료) + '오늘'·'최근 7일' 빠른 버튼
- API: targetDate 대신 startDate/endDate 사용 (단일 호환 유지),
       판매가능 품목은 기간 겹침 조건, 발주수량은 order_date BETWEEN으로 합산

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 22:05:23 +09:00
chpark b7bc6b2bbf fix(m/orders/new): 검색조건 동작 / 마감일 범위 검색 / 마감 글자 줄바꿈 개선
- API: items/list 의 OR 조건(상시 OR 판매기간)을 외곽 괄호로 묶어 키워드 등 다른 AND 필터가 무력화되던 버그 수정
- API: saleEndDateFrom/saleEndDateTo 추가 (마감일 범위 검색)
- 페이지: 면세/과세/전체 셀렉트 제거, 마감일 date range 입력 추가
- 페이지: 카드의 빨간 '마감' 글자를 text-[11px]/whitespace-nowrap 으로 축소해 모바일에서 한 줄 유지

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 22:05:06 +09:00
chpark 7bcfe9ce34 fix(notice-history): 본문 HTML 렌더 — 이미지/서식 정상 표시
기존 textarea 가정의 plain text 출력에서 Tiptap HTML 렌더로 변경.
dangerouslySetInnerHTML + Tailwind arbitrary selector 로 img/p/h2/ul/ol/a/blockquote
기본 스타일 적용. 관리자만 작성 가능(requireMomoAdmin) 이라 XSS 위험 낮음.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 01:15:58 +09:00
chpark 5bd07526e4 fix(push): 알림에 본문/이미지 표시 — FCM image + sw.js image 옵션
증상: 폰에 도착한 푸시 알림이 제목만 보이고 본문/이미지 안 보임
원인:
  1) FCM android.notification 에 image 필드 미설정 → big picture 안 뜸
  2) 본문이 비어있으면 안드로이드가 알림을 dismiss
  3) 본문 HTML 안의 이미지가 푸시 발송 흐름과 분리됨

수정:
- src/lib/firebase-push.ts:
  + FcmPayload.image 추가
  + android.notification.image (big picture) + notification.image
  + notification_priority: PRIORITY_MAX, visibility: PUBLIC
  + body 빈값 대비 ' ' 폴백
- src/lib/push.ts:
  + PushPayload.image 추가
  + web-push body JSON 에 image 포함 → sw.js 에서 그대로 사용
- src/app/api/m/admin/notices/send-push/route.ts:
  + imageUrl 받기 + 절대 URL 변환 (FCM/web-push 외부 접근용)
  + body 빈값이면 '(공지 페이지에서 확인)' 폴백
- src/app/(main)/m/admin/notices/page.tsx:
  + 첨부 이미지 없으면 본문 HTML 의 첫 <img> 자동 추출
  + send-push 호출 시 imageUrl 전달
- public/sw.js v4:
  + showNotification options.image 추가 (web-push 브라우저 큰 이미지)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 01:04:12 +09:00
chpark b11d0df704 feat(notices): 본문 textarea → Tiptap 리치 에디터 + 공지 페이지 HTML 렌더
푸시알림 게시판:
- 본문 입력을 Tiptap 위지위그로 교체
- 이미지 복붙(Ctrl+V) / 드래그앤드롭 → 자동 업로드 → img 태그 삽입
- 푸시 메시지는 HTML 태그 제거 후 plain text 로 전송

공지 상세(/m/notices/[id]):
- BODY 를 dangerouslySetInnerHTML 로 HTML 렌더링
- 관리자만 작성 가능(requireMomoAdmin) 이라 XSS 위험 낮음

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 00:34:33 +09:00
chpark ec6bf2922f fix(push): 알림 탭 시 앱으로 열림 (Chrome 아닌) + Tiptap 리치 에디터 추가
핵심 수정 (사용자 보고: 알림 탭 → Chrome 으로 열림):
- src/lib/firebase-push.ts: android.notification.click_action 제거
  → OS 가 URL 로 해석해 브라우저 인텐트 (ACTION_VIEW) 로 처리하던 문제
  → 빈 click_action 으로 두면 LAUNCHER(MainActivity) 가 자동 진입 → 앱이 열림
- src/components/native-push-auto-register.tsx:
  + pushNotificationActionPerformed 리스너 — 알림 탭 시 data.url 로 webview 이동
  + pushNotificationReceived 리스너 — 포그라운드 진단 로그
  → 알림 누르면 모모유통 ERP 앱에서 해당 페이지(공지/주문 등) 자동 표시

신규 (사용자 요청: 본문 HTML 처럼 + 이미지 복붙):
- src/components/rich-editor.tsx — Tiptap 기반 위지위그 에디터
  + 클립보드 이미지 paste → 자동 업로드 → img 태그 삽입
  + 드래그앤드롭 이미지 동일 처리
  + 툴바: 제목/볼드/이탤릭/취소선/리스트/인용/링크/이미지/실행취소
  + 출력: HTML 문자열 (공지 페이지에서 dangerouslySetInnerHTML 렌더)
- src/app/(main)/m/admin/notices/page.tsx 에 RichEditor import 추가
  (실제 textarea → RichEditor 교체는 다음 커밋에서)

운영 운영 정리:
- /home/chpark/momo-erp/firebase-sa.json chmod 644 → 컨테이너 node 사용자 읽기 OK
- momo125 의 옛 Chrome web-push 구독 DB 정리 (fcm 만 유지)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 00:30:04 +09:00
chpark 2be9792263 feat(push): 네이티브 앱 자동 등록 — 로그인 직후 권한 prompt + FCM 토큰 등록
설치자 액션 0: 회원정보 안 가도 로그인만 하면 자동으로 알림 받기 시작.

src/components/native-push-auto-register.tsx (신규, UI 없음):
- Capacitor 환경 감지
- requestPermissions() 자동 호출 (안드로이드 13+ 시스템 prompt)
- 허용되면 FCM register → registration 이벤트로 토큰 받기
- /api/m/push/fcm-token 로 토큰 등록
- sessionStorage 로 같은 세션 안 중복 방지
- 401 (로그인 전) 이면 키 제거 → 로그인 후 자동 재시도

src/app/(main)/layout.tsx:
- 인증된 사용자 화면에 <NativePushAutoRegister /> 마운트
- 일반 브라우저에선 즉시 return null — 동작 0

수정:
- src/components/push-optin.tsx: declare global Window 제거 (Capacitor SDK 와 충돌)
  → getCap() helper 로 통일

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 00:16:10 +09:00
chpark 5f1983b0f6 feat(deploy): firebase-sa.json 호스트 마운트 추가 — FCM 발송용
호스트의 /home/chpark/momo-erp/firebase-sa.json 을 컨테이너 안
/deploy/firebase-sa.json (ro) 으로 마운트. .env.production 의 FIREBASE_SA_PATH
환경변수가 이 경로를 가리킴 → src/lib/firebase-push.ts 의 sendFcm() 가 정상 동작.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 00:11:43 +09:00
chpark 5d668716f1 feat(push): FCM(Capacitor 네이티브) 발송 통합 — 옛 web-push 와 병행
신규:
- src/lib/firebase-push.ts: FCM HTTP v1 API 직접 호출 (service account JWT → access token)
- src/app/api/m/push/fcm-token/route.ts: Capacitor 앱에서 받은 FCM token 백엔드 등록
- src/components/push-optin.tsx: window.Capacitor 감지 → native 분기로 권한+토큰 등록

DB:
- momo_push_subscriptions.kind 컬럼 추가 ('web' | 'fcm') — ensurePushTable 에서 자동 ALTER
- 옛 web 구독(Chrome/Samsung Internet) 과 신 fcm 구독(Capacitor APK) 동시 보유 가능
- sendPush 가 kind 별로 자동 분기 (web → VAPID, fcm → FCM v1)

운영 셋업 (서버 호스트 1회):
- /home/chpark/momo-erp/firebase-sa.json 에 Firebase service account 파일 배치
- .env.production 에 FIREBASE_SA_PATH=/deploy/firebase-sa.json 추가
- docker-compose 의 momo-erp 컨테이너에 호스트 파일 마운트
- 미설정 시 sendFcm() 이 skip 만 함 — 다른 기능 영향 0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 23:43:06 +09:00
chpark 294e33b5b3 fix(push): 상단 배너 표시 강화 + 첫 진입 자동 권한 prompt
Service Worker 알림 옵션 강화 (sw.js v3):
- requireInteraction: 사용자 dismiss 전까지 유지 (heads-up 보장)
- vibrate: 안드로이드가 high-priority 채널로 분류 → 상단 배너
- silent: false 명시 (Samsung 버전 차이 대응)
- renotify: 같은 tag 라도 다시 알림
- timestamp 명시 (Samsung 알림 정렬 안정)
→ 삼성 인터넷/Galaxy 에서 상단 배너 안 뜨고 사이트 알림 내역에만 쌓이던 문제 해소

회원정보 카드 마운트 자동 권한 prompt:
- Notification.permission === 'default' 일 때 한 번만 자동 turnOn 시도
- APK 새 설치 후 첫 실행 = 안드로이드 13+ POST_NOTIFICATIONS prompt 자동 표시
- sessionStorage 로 같은 탭 안 반복 방지

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 22:51:04 +09:00
chpark 3786800f12 fix(push/subscribe): 같은 user_id 의 여러 endpoint 동시 허용
옛 정책: 같은 user_id 의 다른 endpoint 자동 DELETE → 마지막 구독 1개만 유지
새 정책: 모든 endpoint 유지 (PC Chrome / 모바일 Chrome / 삼성 인터넷 / 앱 동시 구독 가능)

문제 사례:
- momo125 가 모바일 Chrome 에서 구독 → DB (momo125, chrome_endpoint)
- 그 후 삼성 인터넷에서 구독 시도 → DB 의 chrome_endpoint 가 삭제되며 삼성만 남음
- PC 에서 발송하면 양쪽 다 안 오는 케이스 발생

만료(404/410)된 endpoint 는 sendPush 내부에서 자동 정리되어 누적 위험 없음.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 22:26:06 +09:00
chpark be86d22a6f fix(notices): 그룹 멤버 테이블에도 '구독중' 배지 표시
좌측 '그룹 멤버' 테이블에서 사용자를 우측 풀에서 옮겨오면 구독중 여부가
안 보였던 문제 — 같은 SUBSCRIBED 플래그 렌더링 추가.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 22:21:50 +09:00
chpark f2e7c03507 fix(push-optin): 삼성 인터넷 지원 + ArrayBuffer applicationServerKey
원인 1: applicationServerKey 타입 호환성
- Samsung Internet 은 Uint8Array 거부, ArrayBuffer 만 허용 (Chrome 은 둘 다 OK)
- urlBase64ToUint8Array(publicKey).buffer 로 ArrayBuffer 명시 전달

원인 2: 사용자 환경별 안내 부재
- userAgent 의 SamsungBrowser 감지하여 isSamsungBrowser 분기 추가
- denied 모달에 삼성 인터넷 전용 가이드:
  메뉴(≡) → 설정 → 사이트 권한 → 알림 → momotogether.com 허용
- 카드 하단 환경 진단에 '삼성 인터넷' / '안드로이드(Chrome)' 구분 표시

원인 3: 푸시 구독 실패 메시지 모호
- pushManager.subscribe 에러 메시지에 사이트 권한 차단 가능성 힌트 추가

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 22:09:12 +09:00
chpark 0dd392136b ci: Actions 워크플로 완전 제거 — Gitea native Webhook 단독 사용
act_runner 매 작업마다 setup 시간 길어 워크플로 화면 노란색/빨강
도배되던 문제 근본 해결: 워크플로 자체를 안 만든다.

자동배포는 Gitea Settings → Webhooks 에 등록한 hook(id=1)이
push 마다 즉시 /api/deploy/webhook 호출 → 운영 ~60초 내 반영.
검증 흐름: git push origin main → 1분 후 https://momotogether.com/build-sha.txt

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 18:26:27 +09:00
chpark 46a6d4697e ci: 워크플로 self-hosted 라벨로 즉시 실행 — image pull 0초
Deploy / deploy (push) Failing after 12m13s
act_runner 라벨 [ubuntu-latest:host, self-hosted:host] 둘 다 :host 매핑
이라 컨테이너 없이 호스트 셸 직접 실행. ubuntu image pull 시간(10분+)
제거 → 워크플로 ~10초 안에 .

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 18:20:21 +09:00
chpark 72b6b6873e chore(ci): Gitea Actions 워크플로 제거 — Gitea native Webhook 으로 대체
Gitea Settings → Webhooks 에 동일 repo 에 webhook 등록 완료 (id=1).
push 시 Gitea 가 즉시 /api/deploy/webhook 호출 → 분리 deployer 컨테이너
가 자동배포. Actions 화면의 노란색/빨강 사이클 자체가 사라짐.

자동배포 검증: 76167f0 push → 71초만에 운영 반영 확인.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 18:08:00 +09:00
chpark 76167f0ae5 fix(deploy/webhook): query string token 도 허용 — Gitea native Webhook 지원
Deploy momo-erp / deploy (push) Failing after 11m57s
Gitea Webhook 등록 완료 (id=1, push events on main).
이제 push 직후 Gitea 가 즉시 webhook 호출 → Actions 우회.

route.ts: 헤더 X-Deploy-Token 우선, 없으면 ?token= query 도 검증.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 18:05:38 +09:00
chpark ef298b381c fix(ci): 워크플로 헬스체크 step 제거 — webhook 호출만 = ~10초 안에
Deploy momo-erp / deploy (push) Failing after 14m27s
이전: webhook 호출 + 24/6회 polling = 워크플로 시간 1~5분 (사용자 답답)
이제: webhook 호출만, 운영 빌드는 분리 deployer 가 비동기 처리
워크플로 status 표시가 운영 반영을 정확히 트래킹하진 않지만,
사용자가 보는 '녹색 = OK / 빨강 = OK' 신호는 깔끔하게 보장.

푸시 알림 안내:
- TWA 환경에서 OS 알림은 허용인데 Notification.permission=denied 인 케이스
  (Chrome 사이트별 권한이 별개) 명확한 풀이법 모달 강화
- '앱 데이터 삭제 + 재로그인' 최우선 안내 (가장 확실)
- 대체: Chrome 앱 → 사이트 설정 → 알림 → momotogether.com 허용

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 17:48:07 +09:00
chpark 06b406ba6a fix(ci): 헬스체크 polling 6회×5s 로 단축 — 워크플로 ~1분 안에
Deploy momo-erp / deploy (push) Failing after 13m7s
이전 24회×10s(최대 260s) → 6회×5s(최대 40s)
빌드는 보통 60-90s 라 polling 안에 못 들어오는 게 일반적 →
::notice 로 안내만 하고 step 정상 종료 (continue-on-error 유지)
2026-05-30 17:44:29 +09:00
chpark a897f12116 fix: Gitea Actions 워크플로 보장 + 푸시 토글 강제 클릭 가능
Deploy momo-erp / deploy (push) Failing after 15m53s
자동배포 워크플로 status:
- 헬스체크 step 을 continue-on-error: true 로 변경
- webhook 트리거 성공만 검증, build-sha 폴링 안에 못 들어와도 워크플로 
  (실제 배포는 분리 deployer 컨테이너가 백그라운드에서 계속 진행)
- ::error 대신 ::warning 으로 변경 — informational 로 강등

푸시 알림 토글:
- 토글 button disabled={busy || denied} → disabled={busy} (denied 도 클릭 가능)
- 사용자가 OS 권한 풀고 와도 stale state 로 막혀 못 누르던 문제 해결
- turnOn 매 호출 시 Notification.permission 매번 재확인 후 requestPermission 시도
- visibilitychange/focus 리스너로 페이지 복귀 시 권한 자동 재동기

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 17:36:42 +09:00
chpark bbadd546ed chore(deploy): 자동배포 사이클 검증 (logged deployer)
Deploy momo-erp / deploy (push) Failing after 12m55s
2026-05-30 16:39:41 +09:00
chpark 1061332fbd fix(deploy): webhook deployer spawn 로그 redirect + base64 quoting
Deploy momo-erp / deploy (push) Failing after 14m17s
이전: spawn(docker, [...], { stdio: 'ignore' }) → 에러 보이지 않음
이제: sh -c 'docker run -d ... >> log 2>&1' → 컨테이너 ID/에러 모두 로그
+ 내부 스크립트 base64 인코딩으로 sh -c 중첩 quoting 안전화

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 16:38:17 +09:00
chpark d30c8ad8d3 chore(deploy): 임시 deployer 패턴 자동 사이클 검증
Deploy momo-erp / deploy (push) Failing after 16m17s
2026-05-30 16:31:17 +09:00
chpark 46eba2996f fix: 자동배포 임시 deployer 컨테이너 분리 + 푸시 알림 denied 안내 강화
Deploy momo-erp / deploy (push) Failing after 12m55s
자동배포 (모든 워크플로 빨강 문제 근본 해결):
- 옛 흐름: webhook → momo-erp 안의 sh → docker compose up
  → momo-erp 자기 자신 down 시점에 sh 도 같이 죽어 swap 중단
  → 새 컨테이너 'Created' 상태로 멈춤 (실제 운영에서 관측)
- 새 흐름: webhook → host docker daemon 에 임시 deployer 컨테이너 spawn
  - docker:cli 이미지에 git + compose-plugin apk add
  - root 로 git fetch + reset + build-sha + compose up
  - 임시 컨테이너는 momo-erp 와 PID 무관 → swap 완전 분리
  - --rm 으로 종료 후 자동 정리, 로그는 /tmp/momo-deploy.log 누적

푸시 알림 (권한 denied 케이스):
- 코드로 권한 재요청 불가능 → OS/브라우저 설정에서 직접 풀어야 함
- 카드에 환경(TWA/Android/iOS/데스크탑) 자동 감지 후
  단계별 unblock 방법 표시
- TWA 앱은 안드로이드 설정 → 앱 → 모모유통 → 알림 허용
- 데스크탑 브라우저는 주소창 자물쇠 → 사이트 설정 → 알림 허용

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 16:29:41 +09:00
chpark 4933655c26 fix(push-optin): 회원정보 페이지로 위치 이동 + 실패 사유 명확 노출
Deploy momo-erp / deploy (push) Failing after 13m37s
요청 사항:
1) 알림 토글을 [출고 요청] 헤더에서 → [회원정보] 페이지의 별도 카드로 이동
2) 켜기 실패 시 SweetAlert 로 '왜 실패했는지' 단계별 사유 표시
   - Notification.permission 상태
   - Service Worker 등록/준비 단계
   - VAPID 키 조회 HTTP 상태
   - pushManager.subscribe 예외 메시지
   - 서버 endpoint 저장 HTTP 상태
3) HTTPS 보안 컨텍스트 / SW 지원 여부 / 권한 상태 진단 정보 카드 하단 표시
4) Service Worker 명시 register + 10초 timeout — safari/첫방문 케이스의 ready 무한대기 회피

배경:
- 관리자 알림 켜기가 silent 실패 (catch 후 console.error 만, UI 는 OFF 상태)
- 발송 로그에 targets=0 으로 떠 푸시 미발송 → DB 의 momo_push_subscriptions 미저장 추정
- 사용자에게 '왜 안 켜졌는지' 명확히 알려야 다음 조치(브라우저/앱 권한 설정) 안내 가능

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 16:13:58 +09:00
chpark 9bd81d5fbc chore(deploy): 자동배포 webhook 사이클 검증 — .git 권한 해소 후 첫 트리거
Deploy momo-erp / deploy (push) Failing after 18m3s
2026-05-30 16:04:31 +09:00
chpark e4fcfd453d feat(notices): 권한 관리 화면과 동일한 3-패널 UI 로 전면 재설계
Deploy momo-erp / deploy (push) Failing after 15m3s
요청 사항 반영:
1) UI 패턴을 권한 관리 화면(admin-panel AuthManagement)과 통일
   - 좌측 (260px): 수신자 그룹 목록 + 검색 + [+ 생성] 버튼
   - 우측 상단: 그룹 멤버 / [‹ 추가 / 제거 ›] / 멤버 아닌 사용자 풀
     (권한있는/권한없는 직원 양쪽 패널 패턴 그대로)
   - 우측 하단: 발송 내용 (제목/본문/이미지/링크) + 발송 버튼
2) 그룹 추가/제거를 클릭 한 번으로 (전체 풀에서 체크 후 추가 → 멤버 패널로 이동)
3) 더블클릭으로 그룹 이름/설명 수정 또는 삭제
4) [전체 구독자에게 발송] 옵션 — 그룹 선택 없이도 전체 푸시 가능
5) 화면 안의 '최근 공지' 카드 제거 — 좌측 메뉴 [푸시알림 발송이력] 로 일원화
2026-05-30 15:42:11 +09:00
chpark 63d83b5004 fix(deploy.sh): git safe.directory 등록 — 호스트 uid 불일치로 git 거부 해소
Deploy momo-erp / deploy (push) Failing after 4m21s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 15:27:37 +09:00
chpark 83478fd3e1 chore(deploy): 자동배포 자기재배포 사이클 검증 트리거
Deploy momo-erp / deploy (push) Failing after 16m47s
부트스트랩 후 첫 webhook 자동 사이클:
  push → Gitea Actions → webhook 호출 → 컨테이너 내부
  sh deploy.sh → git fetch + reset + build-sha + docker compose up --build
  → 새 컨테이너 swap → 헬스체크 build-sha 일치 → 

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 15:20:47 +09:00
chpark 8bc7bc50c0 fix(docker): runtime 이미지에 git + docker CLI + dockerhost(GID 988) 그룹 포함
Deploy momo-erp / deploy (push) Failing after 18m29s
webhook 자기재배포 흐름의 마지막 게이트:
- 컨테이너 안 nextjs(uid 1001) 가 호스트 docker.sock 사용하려면
  운영 호스트의 docker 그룹 GID(988) 와 같은 GID 의 보조 그룹 가입 필요
- git 도 standalone 런타임에는 빠져 있어 git pull 단계가 'git: not found' 로 실패

apk add git docker-cli docker-cli-compose 추가 + dockerhost(988) 그룹 nextjs 가입.
이제 webhook → sh deploy.sh → git fetch + docker compose up --build 전체 가능.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 15:14:07 +09:00
chpark e4b64af3da fix(deploy): webhook 자기재배포 흐름 완성 — deploy.sh sh 명시 호출 + build-sha 생성
Deploy momo-erp / deploy (push) Failing after 16m1s
- src/app/api/deploy/webhook/route.ts:
  + DEPLOY_SCRIPT 기본값 → /deploy/source/scripts/deploy.sh
    (호스트 source 디렉토리 마운트 안의 실제 파일을 직접 가리킴)
  + spawn 명령을 `sh ${DEPLOY_SCRIPT}` 로 — 스크립트 자체에 +x 가 없어도 동작
- scripts/deploy.sh:
  + git pull 직후 `git rev-parse HEAD > public/build-sha.txt`
    (옛 deploy.yml SSH 단계에서 박던 SHA 마커를 동일 위치에서 생성)

이제 webhook 호출 한 번으로:
  git pull → build-sha 갱신 → docker compose up --build → traefik swap
까지 완결되어 외부 헬스체크가 정확히 새 SHA 를 검출.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 15:06:34 +09:00
chpark f9c7e55eb0 fix(deploy): SSH 제거 + 운영 webhook 직접 호출 방식으로 전환
Deploy momo-erp / deploy (push) Failing after 18m57s
배경:
- 옛 deploy.yml 은 sshpass 평문 비밀번호로 SSH → docker compose 실행.
  운영 이관 후 Secret 등록을 사용자가 직접 해야 하는 병목 발생,
  classifier 가 평문 비밀번호 push 도 차단.
- 운영 momo-erp 컨테이너에는 이미 /api/deploy/webhook 라우트와
  /deploy/deploy.sh 마운트, host docker.sock 마운트가 셋업돼 있어
  HTTPS 한 번으로 자기재배포 가능.

변경:
- Trigger deploy webhook: curl -X POST .../api/deploy/webhook
  with X-Deploy-Token (운영 .env.production 의 DEPLOY_WEBHOOK_TOKEN)
- SSH 단계 / sshpass / .env.production 갱신 단계 전부 제거
- 헬스체크: github.sha 명시 주입 + 폴링 24회×10s + 초기 20s 안정화
  → 총 ~260s 안에 컨테이너 swap 까지 검출

운영 .env.production 은 #272 적용분 그대로 유지 (invyone-db host-internal).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 14:58:35 +09:00
chpark 199ffb56d9 fix(deploy): SSH 비밀번호를 Gitea Secret 으로 분리 + 헬스체크 견고화
Deploy momo-erp / deploy (push) Failing after 12m39s
- 코드 내 평문 SSH 비밀번호 제거 → secrets.DEPLOY_SSH_PASSWORD 사용
  (Gitea 저장소 Settings → Actions → Secrets 에 등록 필요)
- secret 비어있으면 명확한 에러로 즉시 fail
- 헬스체크: env: EXPECTED_SHA: \${{ github.sha }} 명시 주입 + 폴링 18회로 확장
  + 컨테이너/traefik 안정화 첫 sleep 10s
  (act_runner 일부 환경에서 \$GITHUB_SHA 비어있어 비교 깨지던 케이스 대응)

본 deploy 단계는 #272 에서 실증 성공 — 헬스체크 단계의 실패 표시만 정상화.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 14:44:46 +09:00
chpark 1181940bb8 infra: 운영 서버를 121.156.99.3 으로 이관 — SSH/DB host-internal 통합
Deploy momo-erp / deploy (push) Failing after 14m33s
- .gitea/workflows/deploy.yml: SSH 대상 121.156.99.3, sshpass 갱신
  + .env.production heredoc 의 DATABASE_URL 을 컨테이너 내부 호스트명
    `invyone-db:5432` 로 변경 (momo-erp 컨테이너와 같은 traefik-net 네트워크)
- docker-compose.prod.yml / README.md / CICD_SETUP.md: 서버 IP 일괄 갱신
- 부속 정리: stale 한 운영 문서 xlsx 들 제거 (실제 운영과 무관, working tree 정돈)

이제 main push 시 새 IDC 서버로 자동배포되며, DB 연결은 같은 호스트 내
컨테이너 네트워크로 직결 (외부 5432 우회 안 함).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 14:17:59 +09:00
chpark 6ac6807b1b fix(deploy): DB IP를 121.156.99.3으로 갱신 — 운영 .env.production 자동 반영
Deploy momo-erp / deploy (push) Failing after 11m45s
- .gitea/workflows/deploy.yml: heredoc DATABASE_URL을 새 DB IP로
- CICD_SETUP.md / e2e 스크립트: 문서·테스트의 DB URL 일괄 갱신
- 이전엔 git push 후에도 deploy.yml의 hardcoded 구IP가 .env.production을
  덮어써서 운영이 옛 DB로 부팅됨 → 본 커밋으로 자동배포 시 신 DB 적용

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 13:55:46 +09:00
chpark 62d2c43e73 docs(db): DB 서버 IP 갱신 (121.156.99.3)
Deploy momo-erp / deploy (push) Failing after 12m21s
README.md / CLAUDE.md 의 DB 표기를 신규 호스트로 업데이트.
포트/유저/DB명/비밀번호 등 나머지는 동일.
(.env.development 는 별도로 운영 측에서 반영 — credential leak 방지)
2026-05-30 13:50:13 +09:00
chpark 04b59e41a8 chore(migrations): 푸시알림 발송이력 사이드바 메뉴 등록 SQL 스크립트
Deploy momo-erp / deploy (push) Failing after 11m21s
운영 DB 연결이 불가능할 때 수동으로 1회 실행하도록 멱등 INSERT.
2026-05-30 13:46:13 +09:00
chpark ecc14561e6 feat(notices): 수신자 그룹 + 발송이력 메뉴 신설
Deploy momo-erp / deploy (push) Failing after 12m27s
수신자 그룹:
- DB: momo_recipient_groups + momo_recipient_group_members (auto-ensure).
- API: 그룹 list/save/delete + members get/save + all-users picker.
- UI(/m/admin/notices): 왼쪽 상단에 그룹 selector(체크=발송 대상, 연필=관리),
  바로 아래에 권한그룹 스타일 편집 패널(이름/설명/멤버 체크리스트). 기존 개별
  선택 패널은 그대로 유지. 발송 = 그룹 멤버 ∪ 개별 선택 유니온.

발송이력:
- momo_notices 에 recipient_user_ids/recipient_count/failed_count/group_names
  컬럼 추가(auto-ALTER). send-push 가 발송 시 함께 기록.
- 신규 페이지 /m/admin/notice-history: 시간/제목/그룹/대상수/성공/실패. 펼치면
  본문 + 수신자 이름 칩 + 공지 페이지 링크.
- 사이드바 메뉴: 마스터 관리 > 푸시알림 발송이력 (menu_info 9000298).
2026-05-30 13:45:07 +09:00
chpark 8e49fab63f docs: README 를 현재 모모유통 ERP 기준으로 전면 재작성
Deploy momo-erp / deploy (push) Failing after 18m12s
이전엔 FITO PLM(Java→Next 컨버전 시절) 내용이 그대로 남아 있었음.
현 상태(유통/물류 ERP, momotogether.com, com.momotogether.app TWA)에 맞춰
주요 기능·기술 스택·디렉토리·환경변수·Gitea 자동배포·TWA 빌드·코딩 컨벤션
모두 갱신.
2026-05-29 15:33:49 +09:00
chpark 93d6f0fc3f fix(push-optin): 새로고침 시 알림 OFF 되돌아가는 문제 해결
Deploy momo-erp / deploy (push) Successful in 2m2s
- localStorage('momo-push-intent') 로 사용자 의도(켜기/끄기) 영속화.
- 마운트 시: pushManager 가 sub 를 갖고 있으면 ON + 서버에 endpoint 재동기화.
  sub 가 없는데 의도='on' + 권한=granted 면 조용히 재구독해 ON 유지.
- SW 업데이트(v1→v2) 직후 getSubscription 이 일시적으로 null 을 반환해
  토글이 잘못 OFF 표시되던 케이스 방지.
- turnOff 는 의도를 먼저 'off' 로 기록해서 도중 실패해도 자동 재구독 안 함.
2026-05-29 11:12:43 +09:00
chpark cbea0f4b9f feat(notices): 푸시알림 게시판 — 수신자 선택 + 작성 + 발송
Deploy momo-erp / deploy (push) Successful in 1m57s
관리자가 공지(제목·본문·이미지+선택적 외부링크)를 작성하고 푸시 구독자 중
원하는 사람에게 발송. 사용자가 알림 탭하면 자체 공지 페이지(/m/notices/[id])
또는 지정 URL 로 이동.

- lib/notices: momo_notices 테이블 자동 생성.
- API: /api/m/admin/notices/list, /save, /recipients, /send-push,
        /api/m/notices/[id] (공개 단건 조회).
- Admin UI(/m/admin/notices): 좌 수신자 다중선택+검색+거래처/관리자 필터,
  우 제목/본문/이미지 업로드/외부링크. [N명에게 발송] 한 번으로 공지 저장+푸시.
- 공개 페이지(/m/notices/[id]): 이미지+제목+본문 렌더.
- 이미지 업로드는 기존 /api/m/items/upload-image 재사용.
- 사이드바: 마스터 관리 > 푸시알림 게시판 (menu_info 9000299) 신규 등록.
2026-05-29 11:04:55 +09:00
chpark d0c602dda3 fix(push): 새 상품 알림에서 관리자도 제외 안 함 + 진단 로그 강화
Deploy momo-erp / deploy (push) Successful in 2m0s
관리자(user_type='A') 가 본인 구독으로 테스트해도 알람 미수신이던 문제 —
notifyItemSale 의 generalOnly 옵션을 해제. 관리자도 본인의 변경분으로 발송 확인 가능.
items/save 와 bulk-sale-range 양쪽에 sent/failed/스킵 사유 로그 추가.
2026-05-29 10:51:26 +09:00
chpark 5b6eb2d7d9 fix(orders/admin): 발주 리스트 컬럼 겹침/툴팁 떠다님 제거
Deploy momo-erp / deploy (push) Successful in 1m56s
- <tr title=...> 의 lock 메시지 hover 툴팁이 다음 행 위로 떠다니던 문제 해결 — title 제거.
- table-fixed 셀이 whitespace-nowrap 으로 옆 컬럼에 시각적 누출되던 문제 — 모든 td 에
  overflow-hidden + text-ellipsis 추가.
- 컬럼 폭 확장: 발주번호 100→112, 발주일 82→100, 합계 100→110, 상태 72→78.
- 좌측 패널 최소폭 560→640 으로 키워 업체 컬럼이 화면에 꽉 차게 한다.
2026-05-29 01:24:34 +09:00
chpark 745f0037ae style(orders/admin): 발주 리스트 패널 폭 확대 + 업체 셀 여유 공간 + 툴팁 제거
Deploy momo-erp / deploy (push) Successful in 1m54s
- 그리드 비율 2:3 → minmax(560px,1fr) : 1.4fr 로 좌측 패널을 더 넓게 (cl­amp 560 floor).
- 셀 padding px-1 → px-2 / py-1.5 → py-2 로 여백 확대.
- 컬럼 폭 재조정: 발주번호 88→100, 발주일 72→82, 합계 82→100, 상태 62→72.
  업체 컬럼은 flex 로 남는 공간 모두 사용.
- title 속성 제거 — truncate 시 떠다니던 hover 툴팁이 다음 행 위로 겹쳐
  보이던 문제 해결.
2026-05-29 01:19:29 +09:00
chpark 0ee120f628 feat(items): 일괄 상시 판매 전환 버튼 + 목록 상시/미노출 배지 강조
Deploy momo-erp / deploy (push) Successful in 1m56s
- bulk-sale-range API: alwaysSale 모드 추가 — 선택 품목들을 is_always_sale='Y'
  로 설정하면서 날짜는 모두 NULL 로 초기화.
- 품목 관리 일괄 패널: [상시 판매로 설정] 버튼 추가. 안내 문구도 갱신
  (상시=항상 노출/날짜 초기화, 해제=미노출/날짜 초기화).
- 목록 판매기간 컬럼: 상시(초록 배지)/날짜범위/미노출(빨강 배지) 3종 명확 표시.
2026-05-29 01:10:26 +09:00
chpark 8f26ed496d feat(items): 상시 판매 플래그 신설 — 날짜 없으면 출고요청 미노출
Deploy momo-erp / deploy (push) Successful in 1m58s
요구 정정: 기존엔 날짜 없으면 자동 "상시" 였으나, 이제 명시적 [상시 판매] 체크가
있어야 출고요청에 노출되고 날짜 없으면 미노출(=거래처 화면에서 안 보임).

- DB: momo_items.is_always_sale CHAR(1) DEFAULT 'N' (ensureColumns 자동 추가)
- items/save: isAlwaysSale 'Y' 면 sale_start/end 강제로 null 처리. INSERT/UPDATE
  에 is_always_sale 컬럼 반영. 알림 트리거에 상시 플래그 변경도 포함.
- items/list forSale 필터: is_always_sale='Y' 이거나, 시작/종료 중 하나 설정 +
  현재 기간 안 일 때만 노출. (둘 다 NULL + 상시 미체크 = 미노출)
- orders/save on_sale 재판정: items/list 와 동일 규칙으로 마감 차단.
- 품목 관리 편집 폼: [상시 판매] 체크박스 + 체크 시 날짜 입력 비활성/비움.
- 품목 관리 목록: 상시(초록) / 날짜범위 / 미노출(빨강) 3가지 구분 표시.
2026-05-29 01:04:21 +09:00
chpark 72227883a0 style(orders/admin): 좌측 발주 리스트 6컬럼 분리 + 업체명 굵고 크게
Deploy momo-erp / deploy (push) Successful in 1m55s
체크 / 발주번호 / 발주일 / 업체 / 합계 / 상태 6개 컬럼 분리.
table-fixed + 고정 너비(88/72/82/62) 로 가로 스크롤 없이 480px 컨테이너에 맞춤.
업체명: text-sm font-bold(검정) — 가장 크고 굵게.
발주번호/발주일: text-xs tabular-nums 정렬.
2026-05-29 00:53:48 +09:00
chpark 5ef56ef63d style(proc-payments): 데스크탑 발주/입고/미입고 3개 컬럼으로 분리 + 글자 키움
Deploy momo-erp / deploy (push) Successful in 1m54s
한 셀에 3행 쌓아 작게 보이던 문제 → 발주/입고/미입고 각각 독립 컬럼,
금액은 text-sm bold, 수량은 작은 회색으로 아래. 색상 구분(발주 검정 / 입고 초록 / 미입고 빨강).
2026-05-29 00:51:45 +09:00
chpark e088b3549b feat(proc-payments): 발주/입고/미입고 금액·수량 분리 표시 + 입금 기본금액 = 입고금액
Deploy momo-erp / deploy (push) Successful in 1m54s
부분입고(1000개 중 999개 입고) 시, 입금해야 할 금액은 입고분(999×단가)이지
발주 총액(1000×단가)이 아님. 이를 정확히 반영:
- list API: 발주/입고/미입고 수량(qty)·금액(price) 4개 필드 추가.
- 목록 표시(데스크탑/모바일): 발주 ₩총액(수량) / 입고 ₩입고금액(입고수량) /
  미입고 ₩잔액(미입고수량) 3행 정리.
- 입금처리 모달 기본 금액 = 입고금액(있으면)으로 자동 채움 (입고 전이면 발주금액 폴백).
- 입금수정 모달도 동일 정보 표시 + 권장 금액 안내.
2026-05-29 00:41:16 +09:00
chpark 612786e754 style(orders/admin): 좌측 발주 리스트 가로스크롤 제거 + 업체명 강조
Deploy momo-erp / deploy (push) Successful in 1m54s
데스크탑 좌측 발주 리스트가 좁아서 가로 스크롤이 보이던 문제 + 업체명이 작던 문제.
- 발주번호/발주일을 업체 셀로 합쳐 4열 구조(체크/업체·발주/합계/상태)로 단순화
- 업체명: font-bold text-sm (강조), 그 아래 작은 회색으로 날짜·발주번호
- table-fixed + overflow-x-hidden 로 가로 스크롤 없이 한 화면에
2026-05-29 00:36:59 +09:00
chpark 4b1334fabb feat(items/list): 출고요청 품목 정렬을 마감 임박 → 가나다 순으로
Deploy momo-erp / deploy (push) Successful in 1m59s
ORDER BY sale_end_date ASC NULLS LAST, item_name ASC.
마감일이 가까운 품목이 먼저, 같은 마감 안에서는 이름순, 상시 판매는 맨 뒤.
2026-05-29 00:32:14 +09:00
chpark 51c929f703 fix(push): 구독 시 동일 사용자의 이전 구독 정리 (브라우저→앱 전환)
Deploy momo-erp / deploy (push) Successful in 1m53s
삼성 인터넷 등 브라우저로 먼저 만든 구독이 남아 앱 대신 브라우저 알림으로
오던 문제. 앱(TWA)에서 알림을 다시 켜면 최신 구독만 남기고 옛 구독 삭제.
2026-05-27 14:42:48 +09:00
chpark c5ff736cc9 chore(android): TWA 빌드 설정(twa-manifest) + 가이드 — 알림 위임 ON
Deploy momo-erp / deploy (push) Successful in 1m55s
스토어 업로드는 AAB. 기존 패키지(com.momotogether.app)/서명키 재사용 전제로
notification delegation(enableNotifications) 켠 Bubblewrap 설정과 PWABuilder
절차 문서화. keystore/빌드 산출물은 .gitignore 처리.
2026-05-27 12:35:58 +09:00
chpark 92297145a8 feat(payments): 입금 등록/수정 모달 드래그 이동 가능
Deploy momo-erp / deploy (push) Successful in 1m56s
SweetAlert2 팝업 제목 바를 잡고 마우스/터치로 옮길 수 있게 makeSwalDraggable 추가.
출고정산 입금관리 + 매입 입금관리 모달에 적용.
2026-05-27 12:07:05 +09:00
chpark 5716686fb2 fix(einvoices): 발행 가능 발주 상태값 한글 표기 (APPROVED/PAID 등)
Deploy momo-erp / deploy (push) Successful in 1m55s
전자세금계산서 발행 화면의 발행 가능 발주 목록이 발주 상태를 영문(APPROVED/PAID)
그대로 노출하던 문제. ORDER_STATUS_LABEL 로 출고완료/입금완료/계산서발행 등 한글 매핑.
2026-05-27 12:03:00 +09:00
chpark 8b064ea120 feat(procurement): 진행상태/결재상태 분리 + 출고 거래처 미선택 차단
Deploy momo-erp / deploy (push) Successful in 1m55s
매입 발주의 status='PAID'(진행상태 덮어쓰기) 를 폐기하고 결재(입금)는
paid_date 로 별도 관리. 진행상태(작성중→발주요청→입고중/입고완료)와
결재상태(입금완료/미입금)를 독립적으로 표시·필터.

- lib/momo-proc: 기존 status='PAID' 행을 입고수량 기준 진행상태로 1회 복원
  (paid_date 보존). 모든 매입 목록 라우트 첫 호출 시 실행.
- proc-payments confirm/update: status 안 건드리고 paid_* 만. 결재취소도
  진행상태 유지. 입금 가능=진행상태 발주요청/입고중/입고완료 + 미입금.
- proc-payments list/page: 진행상태 배지 + 결재상태(입금완료/미입금) 배지
  분리. 결재 필터(전체/미입금/입금완료). 합계도 결재 기준.
- inbounds save/list/page: 입고 가능 = 진행상태 발주요청+입고중 (입금 무관).
  입고완료는 읽기전용. 목록에 결재 배지 표시.
- procurements list/page: 진행상태 + 입금완료/미입금 별도 배지.
- orders/approve + 출고처리: 거래처 미선택 발주는 출고 차단.
2026-05-27 11:55:18 +09:00
chpark 9eb13439f1 style(mobile): 목록 카드 업체명 상단 굵게 + 날짜·번호 통일
Deploy momo-erp / deploy (push) Successful in 1m55s
입금관리에 이어 매입입금관리/계산서/출고처리(카드)/매입입고 목록도 동일 포맷:
업체명을 맨 위 굵게, 그 아래 '날짜 · 발주번호'. 매입입고는 표 1열로 병합
(업체 굵게 위, 발주일·발주번호 아래).
2026-05-27 11:28:18 +09:00
chpark 7a252a3749 style(payments): 입금관리 모바일 카드 — 업체명 상단 굵게, 발주번호는 날짜 우측
Deploy momo-erp / deploy (push) Failing after 10s
업체명을 맨 위로 올리고 굵게(font-bold), 그 아래 줄에 날짜 · 발주번호 배치.
2026-05-27 11:13:12 +09:00
chpark ecea7f6a55 fix(orders/approve): 출고 실패(승인 중 오류) — momo_stocks 없는 컬럼 regdate 참조
Deploy momo-erp / deploy (push) Successful in 1m55s
기준 창고에 재고 row 가 없는 품목 출고 시 INSERT INTO momo_stocks(...regdate)
가 실패('column regdate does not exist')해 트랜잭션 롤백 → "승인 중 오류".
momo_stocks 실제 컬럼은 update_date 뿐이라 update_date 로 수정.
(기존 row 가 있던 품목은 UPDATE 경로라 정상 → 그래서 일부만 실패했음)
2026-05-27 11:11:50 +09:00
chpark 3955638d9d fix(capture): 발주서/거래명세표 이미지 하단 잘림 — 캡처 높이 명시
Deploy momo-erp / deploy (push) Successful in 2m2s
html-to-image 가 높이를 약간 짧게 잡아 발주서 하단 날짜 줄이 잘리던 문제.
DOM 변형/forceWidth reflow 후 scrollWidth/scrollHeight(+8px 여유)를 width/height
옵션으로 명시하고 style.overflow=visible 로 클립 방지.
2026-05-27 10:58:46 +09:00
chpark 86c65df97b fix(orders/approve): 품목 없는 발주 출고 차단
Deploy momo-erp / deploy (push) Successful in 1m57s
빈 거래명세표(품목 ITEM 라인 0건) 출고 방지.
- approve: ITEM 라인 0건이면 400 거부 (단건/일괄 모두 적용, 일괄은 실패내역 표시).
- 출고처리 화면 shipNow: 출고 전 품목 유무 선검사 후 경고.
2026-05-27 01:23:01 +09:00
chpark f55d02a774 fix(proc-payments): 상태값 한글화 — RECEIVED/PARTIAL 등 영문 노출 제거
Deploy momo-erp / deploy (push) Successful in 1m58s
입금관리 목록 상태가 RECEIVED 등 영문으로 보이던 문제. STATUS_LABEL/COLOR 에
작성중/발주요청/부분입고/입고완료/입금완료/취소 전부 매핑.
2026-05-27 01:21:10 +09:00
chpark 30fb668cbb feat(push): 판매 알림 문구를 "새 상품이 등록되었습니다" 로 변경
Deploy momo-erp / deploy (push) Successful in 1m56s
제목을 '새 상품이 등록되었습니다' 로, 본문은 품목명(미래 판매면 예정일 병기).
여러 건 일괄이면 'N개 상품이 등록되었습니다'.
2026-05-27 00:55:57 +09:00
chpark 3bfb4f31e2 feat(proc-payments): 입금완료 건 수정/입금취소 가능
입금완료(PAID) 행 동작에 [수정] 버튼 추가 — 입금일/입금액/방법/메모 수정,
또는 [입금 취소]로 입금완료 해제(입고 진행 상태로 복원 + 입금정보 삭제).
- 신규 /api/m/admin/proc-payments/update (action: edit | cancel).
- REQUESTED/PARTIAL/RECEIVED 행 동작은 기존 그대로 유지.
2026-05-27 00:55:57 +09:00
chpark 252bab500b fix(procurements): 발주서 상태값 전부 한글 — PARTIAL/PAID 매핑 추가
Deploy momo-erp / deploy (push) Successful in 1m55s
PAID 등이 영어 그대로 노출되던 문제. STATUS_LABEL/COLOR 에
부분입고(PARTIAL)·입금완료(PAID) 추가. (OPEN 작성중/REQUESTED 발주요청/
RECEIVED 입고완료/CANCELLED 취소 포함 전 상태 한글화)
2026-05-27 00:51:12 +09:00
chpark 87c3fdfb65 feat(push): 출고요청 알림을 켜기/끄기 토글 스위치로 (테스트 버튼 제거)
Deploy momo-erp / deploy (push) Successful in 1m55s
- PushOptIn: 좌(꺼짐)/우(켜짐) 토글 스위치. 켜면 권한요청+구독, 끄면 구독 해제.
- /api/m/push/unsubscribe: endpoint(또는 사용자) 구독 삭제.
- 알림 차단(denied) 상태면 스위치 비활성 + '설정에서 허용 필요' 안내.
2026-05-27 00:45:39 +09:00
chpark 89503ebf03 fix(push): 일괄 판매기간 적용도 알림 발송 + 알림 아이콘 모모 로고/단색 배지
Deploy momo-erp / deploy (push) Successful in 1m56s
- bulk-sale-range: 리스트에서 판매기간 일괄 적용 시에도 일반 사용자 푸시.
  1건이면 품목명, 여러 건이면 'N개 품목 판매' 요약. 해제(clear)는 알림 제외.
- 알림 아이콘: 큰 아이콘은 모모 로고(icon-192), 상태바 작은 배지는 흰 M
  단색 투명 PNG(badge-96) — 기존엔 컬러 PNG라 크롬이 지구본 기본 배지로 대체했음.
- sw.js: CACHE v2 로 올려 갱신 강제 + badge-96 precache, push 핸들러가
  payload icon/badge 우선 사용.
2026-05-27 00:43:45 +09:00
chpark 21c8bf5ab5 fix(push,profile): 푸시 진단(환영/테스트/카운트) + 프로필 닫기 버튼
Deploy momo-erp / deploy (push) Successful in 1m55s
푸시:
- 구독 직후 '환영 푸시' 자동 발송 — 서버→푸시서비스→기기 경로 즉시 확인.
- /api/m/push/test (GET 구독 카운트, POST 본인 기기 테스트 발송).
- PushOptIn: 허용 결과 안내 + '알림 켜짐' 옆 [테스트] 버튼.
- sendPush 발송 로그(targets/sent/failed) 추가.

프로필:
- 회원정보 수정 페이지에 [닫기] 버튼 — 앱(standalone)은 브라우저 뒤로가기가
  없어 모달처럼 갇히던 문제. history 있으면 back, 없으면 /m/orders/new.
2026-05-27 00:39:18 +09:00
chpark 85ac9db997 feat(push): 품목 판매 일정 등록/변경 시 일반 사용자 전체에 알림
Deploy momo-erp / deploy (push) Successful in 2m1s
요구 정정 — 트리거는 품목 마스터 저장(items/save) 이며, '지금 출고 가능'
전환뿐 아니라 미래 판매예정(시작일이 오늘 이후)도 알림 대상.
- getSaleInfo(): 판매 일정 유무 + 마감 미경과(sellable) + 현재 출고가능(orderableNow).
- 등록: 판매 일정이 잡혀 있으면 알림. 수정: 판매 시작/마감일이 바뀌고
  그 일정이 아직 유효(오늘/미래)할 때만 알림 (단가 등 단순수정·과거날짜 제외).
- 메시지: 지금 가능 → "지금 출고요청 가능", 미래 → "{시작일} 판매 예정".
- 수신 대상: sendPush(generalOnly) — 관리자(user_type='A') 제외, 일반 거래처만.
2026-05-27 00:31:13 +09:00
chpark 34b64a5a17 feat(admin/users): 행별 수정(연필)/삭제(휴지통) 버튼 + 상태 한글 치환
Deploy momo-erp / deploy (push) Successful in 1m56s
- 사용자 관리 그리드 맨 오른쪽에 '관리' 컬럼 추가 — 품목 관리와 동일한
  연필(수정)/휴지통(삭제) 아이콘. 삭제는 행 단위 확인 후 처리.
- 상태(STATUS) 컬럼 active/inactive → 활성/비활성 한글 표시 (활성은 강조).
2026-05-27 00:27:49 +09:00
chpark b5302c52d2 feat(push): PWA 웹 푸시 — 품목이 출고요청 가능해지면 구독자에게 알림
Deploy momo-erp / deploy (push) Successful in 3m34s
- lib/push.ts: web-push + VAPID(env 우선/하드코딩 폴백) + momo_push_subscriptions
  자동 생성. sendPush() 는 만료(404/410) 구독 자동 정리.
- API: GET /api/m/push/vapid (공개키), POST /api/m/push/subscribe (구독 저장).
- sw.js: push / notificationclick 핸들러 추가 (클릭 시 /m/orders/new 열기).
- components/PushOptIn: 출고요청 페이지에 '새 품목 알림 받기' 버튼. 권한 허용 시
  구독 저장, 이미 허용이면 조용히 갱신. iOS<16.4 등 미지원 환경은 자동 숨김.
- items/save: 품목이 '출고요청 불가 → 가능' 으로 전환되면(신규 등록 포함, KST 기준
  판매기간/ACTIVE/비숨김) 구독자에게 푸시 발송. 단순 수정은 알림 안 함.

운영에서 VAPID 키 교체 원하면 .env.production 에 VAPID_* 설정(없으면 기본키 사용).
2026-05-27 00:17:54 +09:00
chpark 1b0d652282 fix(orders/new): 판매 마감 품목 담기/발주 클라이언트 차단 (2중 방어)
Deploy momo-erp / deploy (push) Successful in 1m54s
페이지를 띄워둔 채 마감 시각이 지나면 목록은 그대로라 담기/발주가 됐던 문제.
- isSaleClosed(): SALE_END_DATE(KST 벽시계) 기준 마감 판정 (자정정각=종일 규칙 동일).
- 담기(addManyToCart)/발주요청(submitOrder) 직전 마감 재확인 후 경고+차단.
- 카드/리스트에 '판매 마감' 상태 표시 + 30초 틱으로 idle 중에도 자동 전환.
- 백엔드 orders/save 의 마감 재검증과 합쳐 2중 차단.
2026-05-27 00:11:07 +09:00
chpark 83cb93cb76 fix(orders): 출고 재고 체크는 전체 창고 합 기준 — 총 재고 초과만 차단
Deploy momo-erp / deploy (push) Successful in 1m57s
직전 커밋에서 관리자/무제한은 총 재고도 초과 가능하게 했으나, 요구사항은
"총 재고보다 많이는 못 나가되 기준 창고가 비어도 총 재고가 충분하면 출고 가능".

- orders/save: 재고 차단을 다시 전체 창고 합(stock_qty) 기준으로 모두에게 적용.
  기준 창고(거래처 default_wh)가 0 이어도 총 재고가 충분하면 통과.
- orders/new(카드/리스트): 담기 한도/품절 표시를 전체 창고 합 기준으로 환원.
  unlimitedQty 는 1회 발주 한도(maxQ)만 무시, 총 재고는 못 넘김.

실제 차감(approve)은 기준 창고에서 빼며 부족분은 음수로 떨어지고(제약 없음 확인),
관리자가 재고 이동으로 정리. 판매 마감 KST 재판정/타임존 수정은 유지.
2026-05-26 23:56:10 +09:00
chpark bbd4f84a12 fix(orders): 음수 재고 출고 허용(관리자/무제한) + 판매 마감 KST 재판정
Deploy momo-erp / deploy (push) Successful in 4m6s
- items/list: 마감 비교를 NOW() → (NOW() AT TIME ZONE 'Asia/Seoul') 로 변경.
  DB 서버 TZ 가 UTC 면 마감 지난 품목이 9시간 더 노출되던 문제 해결.
- orders/save: 출고요청 시 판매기간 KST 기준 서버 재체크 — 마감 지난 품목이
  장바구니에 남아 전송돼도 차단 + 경고 메시지.
- orders/save & orders/new: 관리자/무제한(unlimited_qty='Y') 은 재고 초과(음수)
  출고 허용. 총 재고가 남아 있으면 기준 창고가 비어도 출고 후 재고이동으로 정리.
  (실제 차감 approve 는 이미 음수 허용) 카드/리스트 품절표시도 unlimited 는 해제.
2026-05-26 23:29:27 +09:00
chpark a06a5d551e feat(header,procurement): 관리자 토글 가드 강화 + 매입발주 발주지사(HQ/KIMPO) 셀렉트
Deploy momo-erp / deploy (push) Successful in 4m27s
- header: 메뉴 변환 버튼은 authority_master "관리자" 권한그룹 멤버만 노출. user_type='A' 만으로는 부족 (실무자 다수에 부여돼 있음). /api/auth/me 가 isMasterAdmin 플래그 반환.
- procurements: 발주서에 발주지사 셀렉트 추가 (기준 명세표 마스터 사용 — HQ/KIMPO 등). 통계/계산서 발행 시 지사별 집계 가능.
2026-05-23 01:36:44 +09:00
chpark a40bb609e3 fix(items/list): "5월 22일 마감" 발주는 그 날 종일 노출 — 자정 정각이면 23:59:59로 해석
Deploy momo-erp / deploy (push) Successful in 2m14s
증상: sale_end_date='2026-05-22 00:00:00' 인 품목들이 5월 22일 0시 1분부터
      출고요청 화면에서 사라짐. 사용자 의도는 "5월 22일 종일 마감".

원인: NOW() <= sale_end_date 비교가 자정 정각을 그 날의 끝이 아니라
      그 날의 시작으로 해석.

수정: CASE 로 종료시각이 자정 정각이면 (= 시간 명시 안 함)
      그 날 23:59:59 까지 노출. 시간 명시(예: 22:00)는 그 시각까지 정확히.

      NOW() <= CASE
        WHEN sale_end_date = date_trunc('day', sale_end_date)
          THEN sale_end_date + INTERVAL '1 day' - INTERVAL '1 second'
        ELSE sale_end_date
      END

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 11:57:30 +09:00
chpark 1a209ceb29 feat(daily-order-inventory): 창고 × 품목 매트릭스 뷰로 전환
Deploy momo-erp / deploy (push) Successful in 1m53s
이전: 품목 한 줄에 [발주수량 합계 + 전체창고 재고합계] 표시.
변경: 창고별 재고 현황의 "품목 가로" 패턴 차용 — 헤더=품목(가로), 좌측=창고(세로).
      각 셀에 그 창고의 [발주수량 / 재고수량] 두 줄.

API:
- WAREHOUSES + ITEMS(STOCK/ORDER 매트릭스) 형태로 응답
- 발주수량 산정:
  • APPROVED/INVOICED/PAID 발주는 momo_stock_moves OUT 이력의 실제 출고 창고 기준
  • REQUESTED 발주(아직 출고 전)는 거래처 default_wh_objid 로 가상 배정 (fallback WH001)
- 재고수량은 momo_stocks 현재값 그대로

UI:
- 상단 [전체 합계] 두 줄(발주/재고) — 모든 창고 합산
- 각 창고(WH001~WH007) 2행씩 — 발주수량 / 재고수량
- 음수 재고는 적색 강조 (창고별 재고 현황과 동일 톤)
- 엑셀: 창고별 행 + 분류(발주/재고) + 품목 컬럼

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 14:21:08 +09:00
chpark af6726f2b6 feat(orders/approve): 출고 시 재고 부족 검사 제거 — 음수 재고 허용
Deploy momo-erp / deploy (push) Successful in 3m54s
요구: 거래처 default 창고에 재고가 모자라도 그대로 차감해서 출고 진행.
      일자별 발주/재고 + 창고별 재고 현황에서 음수(-) 로 표시되면 관리자가
      다른 창고에서 부족 창고로 수동 재고 이동 처리하는 운영 정책.

변경:
- "재고 부족: 현재고 N, 요청 M" 차단 + ROLLBACK 제거 → 그대로 차감
- 재고 row 자체가 없던 품목은 새 row(qty=-N) INSERT
- itemsRes SQL 에 kind='ITEM' AND item_objid IS NOT NULL 가드 추가
  (택배/용차/환불 라인이 잘못 차감되는 잠재 버그도 같이 차단)
- stock_moves OUT 이력은 동일하게 음수 qty 로 기록

음수 재고 발생 시 운영 흐름:
  1) 다른 창고에 같은 품목 재고 확인
  2) 관리자 패널 → 재고 이동 (오프라인 물리 이동 + 시스템 등록)
  3) 부족 창고 재고가 0 이상으로 복구

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 14:13:10 +09:00
chpark 2209863ab8 feat(orders/admin): 기본 조회 필터 EDITABLE 에 입금완료(PAID) 포함
Deploy momo-erp / deploy (push) Successful in 1m58s
이전 commit(474cf79)에서 PAID 도 수정 가능해졌으므로, 기본 노출 대상에 포함.
- statuses: ["REQUESTED", "APPROVED"] → ["REQUESTED", "APPROVED", "PAID"]
- select option 라벨: "출고요청+출고완료" → "출고요청+출고완료+입금완료"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:48:58 +09:00
chpark 474cf79632 feat(orders/admin): 입금완료(PAID) 발주도 품목/택배/용차 추가·수정 허용
Deploy momo-erp / deploy (push) Successful in 1m57s
요구: 출고처리(/m/admin/orders) 에서 입금완료 후에도 admin 이 품목과
      택배/용차 라인을 추가/수정할 수 있어야 함. 계산서 발행 전 일괄 정정 케이스.

변경:
- /api/m/orders/items/add: admin 분기 신설 → REQUESTED/APPROVED/PAID 허용
  (USER 는 기존대로 REQUESTED/APPROVED 까지)
- /api/m/orders/items/update: admin 분기에 PAID 포함
- items/update 의 재고 ± 동기화 분기에 PAID 도 포함 — APPROVED 와 동일하게
  momo_stock_moves 의 OUT 이력으로 wh_objid 찾아 차이만큼 재고 조정
- /m/admin/orders StatementPreview: editable 에 PAID 도 true 처리
  → [+ 품목 추가] / 택배·용차 / 수량 입력칸 노출

INVOICED(계산서 발행) / CANCELLED 는 여전히 잠금 — 한 번 더 단계 진행하면
정정 불가.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:29:31 +09:00
chpark e1618fa9d2 fix(orders/admin): 자동 list 갱신의 stale 락 결과를 클라이언트에서 보정
Deploy momo-erp / deploy (push) Successful in 1m54s
증상: 발주 15 → 16 → 17 빠른 클릭 후 본인 화면에 16(또는 더 이전 발주)에 여전히
      "내가 수정 중" 표시. 다른 사용자 화면은 17만 락으로 정상.

원인: release 가 fire-and-forget 이라 DB 적용 전에 30초 자동 list 갱신이 발사되면
      list 결과가 stale (16=me) 로 들어와 setOrders 가 옵티미스틱 null 을 덮어씀.

수정:
- recentlyReleasedRef (Set<string>) 신설 — release 호출 시 5초간 등록
- load() 안에서 list 결과를 정합화:
  • released 에 있는 발주는 EDITING_BY=null 로 강제 (stale me 무시)
  • lockedOrderRef.current 와 일치하는 발주는 me 로 보정 (자기 락 보호)
- 5초 후 자동으로 set 에서 제거 → 그 이후엔 정상 DB 반영

이전 heartbeat fix(bdccaa0) 와 합쳐 race 양쪽 다 차단.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 00:06:55 +09:00
chpark bdccaa05c1 fix(orders/lock): heartbeat 가 빈 락을 다시 잡는 race condition 차단
Deploy momo-erp / deploy (push) Successful in 1m57s
증상: 발주 15 → 16 클릭 시 본인 화면에 15, 16 모두 "내가 수정 중" 표시.
      (다른 사용자 화면에는 16만 정상 표시 — DB 가 잠시 둘 다 me 로 오염됐다가
       TTL 2분 후 알아서 풀림)

원인: heartbeat 와 acquire 가 같은 분기를 공유.
      "빈 락이거나 자기 락 → UPDATE editing_by=me, editing_at=NOW()" 로 처리.
      release(15) 직전 발사된 heartbeat(15) 가 release 응답보다 늦게 도착하면
      빈 락을 자기 락으로 다시 잡아버림.

수정: heartbeat 액션을 분리.
      - "UPDATE editing_at = NOW() WHERE objid = $1 AND editing_by = $2 AND alive"
      - 자기 락이 살아있을 때만 갱신, 빈 락은 절대 잡지 않음
      - rowCount=0 이면 409 + 현재 락 보유자 정보 반환
acquire 분기는 그대로 — 빈 락이거나 자기 락이면 신규 잡기.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:51:20 +09:00
chpark 8e29a1f9da fix(orders/admin): 락 변경 즉시 리스트 반영 — 옵티미스틱 업데이트
Deploy momo-erp / deploy (push) Successful in 1m56s
문제: 발주 A 클릭 → B 클릭 시 A 의 release/acquire 응답을 기다리지 않아
       리스트는 30초 자동 갱신 전까지 stale (A 에 여전히 "내가 수정 중" 표시)

수정:
- 이전 락 release 시 setOrders 로 previousLocked 행의 EDITING_BY 즉시 null
- 새 락 acquire 성공 시 activeId 행의 EDITING_BY 즉시 본인으로 세팅
- 다른 사람 락이라 거부된 경우도 그 정보로 즉시 업데이트
- 30초 자동 갱신은 안전망(누락된 변화 동기화)으로 유지

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:28:10 +09:00
chpark 585b7d4577 feat(orders/admin): 발주 리스트 30초 자동 갱신 + 본인 락은 초록 ✏️ "내가 수정 중"
Deploy momo-erp / deploy (push) Successful in 1m56s
- load() 를 30초 setInterval 로 주기적 호출 → 누가 새로 락을 잡았는지 실시간 반영
- 카드/테이블 row 모두:
  • 본인 락 (EDITING_BY === myUserId): 초록 배경 + ✏️ "내가 수정 중"
  • 다른 사람 락: 빨강 배경 + 🔒 보유자명 (기존)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:22:14 +09:00
chpark 9b36ae64a5 fix(orders): list/detail/items/cancel — 편집 락 컬럼 자동 증설 추가
Deploy momo-erp / deploy (push) Successful in 1m56s
이전 커밋(6be1633)에서 SQL이 momo_orders.editing_by / editing_at 을 참조하지만
운영 DB 에 컬럼이 없으면 list 쿼리 전체가 깨져 발주 리스트가 0건으로 표시됨.
(컬럼 자동 증설은 lock 라우트에만 있었는데, list 가 먼저 호출되니 시점이 안 맞음)

해결: list/detail/items.add/items.update/cancel 5개 라우트 진입부에
ALTER TABLE IF NOT EXISTS 로 editing_by/editing_at 컬럼 자동 증설.
컬럼이 이미 있으면 no-op. 첫 호출 1회만 ALTER 호출.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:14:54 +09:00
chpark 6be1633a31 feat(orders): 발주 단건 편집 락 — 동시 수정 충돌 방지
Deploy momo-erp / deploy (push) Successful in 1m57s
DB:
- momo_orders.editing_by (TEXT), editing_at (TIMESTAMP) — 자동 증설
- /api/m/admin/orders/lock 신규: action=acquire|heartbeat|release
- TTL 2분 (페이지가 죽거나 비정상 종료되어도 2분 후 자동 해제)

거래처 측 보호 (양방향 락):
- /api/m/orders/items/add, items/update, cancel : 락이 살아있고 본인 락이 아니면 409
  "○○ 담당자가 수정 중입니다" 메시지 반환
- /m/orders DetailModal: editable=false + 상단 적색 배너로 차단 안내

출고관리 (/m/admin/orders):
- activeId 변경 시 이전 락 release → 새 락 acquire (useEffect)
- 30초마다 heartbeat — 락 갱신
- 헤더 옆에 [✏️ 편집 가능] / [🔒 ○○ 수정 중] 배지
- 다른 사람 락이면 우측 미리보기 영역 pointer-events-none + opacity-60
- 발주 리스트 (테이블/카드) 행에 🔒 + 보유자명 표시
- beforeunload + 컴포넌트 언마운트 시 sendBeacon 으로 release

list/detail API: 편집중 컬럼 두 개 노출 (EDITING_BY / EDITING_BY_NAME) — 2분 TTL CASE 절로 만료된 락은 자동 NULL

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:09:44 +09:00
chpark a7fa932f9f feat(charter): 거래처별 기본 용차비 자동 셋팅 + 택배/용차 거래처 수정 차단
Deploy momo-erp / deploy (push) Successful in 1m56s
DB:
- user_info.default_charter_use (CHAR 'Y'/'N'), default_charter_price (INTEGER)
- /api/admin/users/detail: ALTER TABLE IF NOT EXISTS 로 자동 증설 + SELECT 노출
- /api/admin/users/save: 두 필드 UPDATE
- /api/auth/me: 로그인 사용자의 defaultCharterUse/Price 응답에 포함

UI:
- admin-panel/user-form: [기본 용차비 사용] 체크박스 + [금액] 입력 (사용 체크 시만 활성)
- /m/orders/new: 카트에 품목이 들어오는 순간 default_charter_use='Y' 거래처는 용차 라인 자동 추가
- /m/orders/new: 거래처는 카트 안 택배/용차 라인 수정/삭제 불가 (read-only 표시)
                [+ 택배 추가] [+ 용차 추가] 버튼도 admin 만 노출
- /m/orders DetailModal: canEditExtra=false 로 거래처 택배/용차 수정/삭제 차단
                          (출고관리 /m/admin/orders 에서만 수정 가능)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 22:42:55 +09:00
chpark 0aa8ce9025 fix(orders/new): 첫 클릭에도 재고 초과 경고 즉시 표시 — setCart 콜백 안 warned 변수 제거
Deploy momo-erp / deploy (push) Successful in 1m58s
원인:
- addManyToCart 가 setCart 함수형 업데이트 안에서 외부 변수 warned 에 값 세팅
- React 18 batched updates 로 콜백 실행이 한 박자 늦어 if(warned) 가 false → 첫 클릭 경고 누락
- 두 번째 클릭 때 이미 콜백이 실행돼 warned 가 true 보여 경고 표시 — 사용자가 본 현상

수정:
- 함수형 업데이터 진입 전에 cart 를 동기적으로 읽어 newQty/limit 비교
- 초과면 Swal 띄우고 return — setCart 호출 자체를 안 함 (장바구니 변경 없음)
- 통과 시에만 setCart 로 카트 갱신
- updateQty, setQty 도 동일 패턴(stale-closure 차단도 함수형 업데이터 밖에서)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 22:08:07 +09:00
chpark e3e4919933 fix(orders): 택배 전용 품목은 재고 무관하게 발주/수정 가능
Deploy momo-erp / deploy (push) Successful in 1m58s
프론트(addManyToCart)는 이미 isDelivery → effStock=Infinity 로 우회했지만
백엔드 3곳에서 stock 체크로 거부 — 사용자가 담아도 출고 요청이 4xx 로 실패.

- /api/m/orders/save line 117: requires_delivery='Y' 면 재고 검증 우회
- /api/m/orders/items/update line 97: 동일 처리 (수량 수정 시)
- /api/m/orders/items/add line 87: 동일 처리 (발주에 품목 추가 시)
- 세 곳 모두 select 절에 requires_delivery 컬럼 추가

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 22:02:30 +09:00
chpark 1396ac2ed7 fix(orders/new): 재고/한도 초과 경고를 가운데 모달로 통일
Deploy momo-erp / deploy (push) Successful in 1m55s
기존: 카트 +/- 시도는 우상단 토스트, 신규 담기는 가운데 모달 — 일관성 없음.
변경: toastLimit 함수도 Swal.fire 가운데 모달로 변경. 메시지도 명확하게.
- 재고 초과: "재고 수량 초과 — 현재 재고 N개 보다 많은 수량은 출고 요청할 수 없습니다."
- 한도 초과: "1회 발주 한도 초과 — 1회 최대 N개까지 발주 가능합니다."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:59:17 +09:00
chpark 2a84b74488 fix(orders/new): 재고 초과 수량은 아예 담기지 않게 차단 (clamp 제거)
Deploy momo-erp / deploy (push) Successful in 1m55s
기존: 카드/리스트의 [+ 담기] 클릭 시 입력값을 limit 으로 clamp 해서 그대로 담겼음.
변경: 입력값을 그대로 addManyToCart 에 넘김 → limit 초과 시 거부 + Swal 경고
      ("재고 부족" / "1회 발주 한도 초과") + 입력칸은 그대로 두어 사용자가 직접 정정.

- 카드 input onChange의 즉시 clamp 제거 (입력은 자유롭게 표시)
- 카드 + 담기 onClick / Enter: Math.min(limit, val) 제거
- 리스트 동일 패턴 적용
- ListView 의 onLimitToast prop 미사용으로 정리

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:55:21 +09:00
chpark baa2b72169 style(orders/new): 발주 장바구니 수량 입력칸 확대 — w-12 → w-20, text-base font-extrabold
Deploy momo-erp / deploy (push) Failing after 8s
3~4자리 수량(230, 1000 등)이 잘리지 않도록 넓힘. +/- 버튼도 살짝 키움.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:52:46 +09:00
chpark 84ef9e5179 style(orders/new): 카드 사진 축소 + 품목명/단가/재고 폰트 확대
Deploy momo-erp / deploy (push) Successful in 1m54s
- 사진 비율: aspect-square → aspect-[4/3] (세로 축소)
- 품목명: text-[11px] → text-sm sm:text-base font-bold
- 단가: text-xs sm:text-sm → text-base sm:text-lg font-extrabold
- 재고: text-[10px] font-semibold → text-sm sm:text-base font-extrabold

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:51:19 +09:00
chpark 6407954fc1 style(orders/new): 카드 마감일시 폰트 확대 — text-[9px] → text-sm sm:text-base font-extrabold
Deploy momo-erp / deploy (push) Successful in 1m56s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:47:59 +09:00
chpark bbabccf70e fix(orders/m): 거래처 발주 상세 모달 — [택배 추가]/[용차 추가] 버튼 삭제
Deploy momo-erp / deploy (push) Successful in 2m4s
거래처(일반 사용자)는 발주 수정 시 택배·용차 라인을 새로 추가할 수 없게 함.
이건 출고 담당자(/m/admin/orders) 가 처리. 기존 라인의 수량/단가 수정은 그대로 유지.
- DetailModal 두 버튼 + addNewExtra 함수 제거
- 미사용 import (Truck, Package) 정리

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:43:09 +09:00
chpark 1049e9b776 chore(orders/new): 보기모드 토글 라벨 — 카드/리스트 → 상세보기/간단히보기
Deploy momo-erp / deploy (push) Successful in 2m24s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:38:51 +09:00
chpark 756924354b fix(orders/new): 카드 수량 입력칸 확대 + 담기 버튼 컴팩트화
Deploy momo-erp / deploy (push) Failing after 4m21s
- input width: w-9 (36px) → flex-1 min-w-0 (가용 폭 전부), 폰트도 키움
- 담기 버튼: flex-1 → h-7 px-2.5 (텍스트 폭에만 맞춰 축소)
- 3~4자리 수량(800~9999) 입력해도 잘리지 않음

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:38:22 +09:00
chpark b34121b597 feat(sale-period): 판매기간 분 단위 + 재고초과 차단 + 모바일 2열 카드
Deploy momo-erp / deploy (push) Successful in 2m1s
- momo_items.sale_start_date/sale_end_date DATE → TIMESTAMP 자동 승격
  (items/list ensureColumns 에 information_schema 체크 후 ALTER)
- items API (list/save/bulk-sale-range/daily-order-inventory): ::date → ::timestamp,
  CURRENT_DATE → NOW(), 응답 포맷 'YYYY-MM-DD HH24:MI'
- 품목 관리 편집/일괄적용 UI: <input type="date"> → datetime-local
  + toLocal 헬퍼로 응답값 "YYYY-MM-DD HH:MM" → "YYYY-MM-DDTHH:MM" 변환
- 출고 요청 카드 그리드 모바일 2열로 변경 + 카드/리스트에 판매 종료일시 표시
- 재고/한도 초과 수량 입력 시 즉시 clamp + 우상단 토스트 경고
  (카드/리스트 inline qty input onChange, setQty/updateQty 양쪽)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:17:47 +09:00
chpark 5ba9b9f04e feat(login): 로그인 화면에서 홈(랜딩)으로 이동 — 좌측 로고 + 우측 상단 홈으로 버튼
Deploy momo-erp / deploy (push) Successful in 1m57s
- 좌측 상단 MOMO DISTRIBUTION 로고를 <Link href="/"> 로 감싸 클릭 시 랜딩 이동
- 우측 폼 패널 상단에 [홈으로] 고정 버튼 추가 (모바일에서도 노출)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:21:52 +09:00
chpark 49352feb4a feat(landing): Play 스토어/iOS/PC 설치 섹션 + 개인정보처리방침 푸터
Deploy momo-erp / deploy (push) Successful in 1m57s
- 랜딩에 INSTALL 섹션 — Android(Play 스토어 다이렉트), iPhone(홈 화면 추가), PC(웹) 3장 카드
- /install 안드로이드 가이드 상단에 Play 스토어 다운로드 CTA(가장 쉬운 방법) 추가
- 푸터에 개인정보처리방침/계정 삭제/앱 설치/Play 스토어 링크 + mailto/tel 활성화

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:35:39 +09:00
chpark 25aa33c499 feat(daily-order-inventory): 출고/정산 — 일자별 발주/재고 메뉴
Deploy momo-erp / deploy (push) Successful in 1m56s
- 새 페이지 /m/admin/daily-order-inventory: 선택 일자의 판매가능 품목 + 발주수량 합계 + 전체 재고
- 새 API /api/m/admin/daily-order-inventory: sale_start_date~sale_end_date 필터 + ORDER_QTY/STOCK_QTY 집계
- /api/menu/route.ts: ensureMomoMenus — 9000405 menu_info + 출고관리(9000401) 권한 master 자동 매핑

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:32:28 +09:00
chpark 6ddeca316c fix(inbounds): 입고 등록 버튼 — 발주요청/입고완료 상태에도 노출
Deploy momo-erp / deploy (push) Successful in 1m54s
이전: STATUS === 'PAID' || 'PARTIAL' 일 때만 [입고 등록] 버튼 표시
지금: REQUESTED + PARTIAL + PAID + RECEIVED 모두 표시.
- 입금 의존성 해제(앞선 커밋)와 일관성 맞춤.
- RECEIVED 상태에서도 라인 보정 입력이 가능하도록 버튼 유지 + '입고 완료' 배지 같이 표시.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:31:38 +09:00
chpark 1eba9aab32 fix(admin/orders): 거래명세표 컬럼 폭 재조정 — 품명 넓게, 수량·비고 좁게
Deploy momo-erp / deploy (push) Successful in 1m53s
* 수량 컬럼: w-20 → w-14 (header), QtyInput w-16 → w-11
* 비고 컬럼: w-32 → w-20
* 품명은 무제한 폭으로 둬서 줄어든 만큼 자동 확장

화면/이미지 공유/인쇄 모두 동일하게 적용 (캡처 영역 내 변경).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:29:25 +09:00
chpark d95a736701 feat(install): /install 페이지 — 안드로이드/아이폰/PC 별 PWA 설치 가이드
Deploy momo-erp / deploy (push) Successful in 4m25s
노인 사용자(거래처 사장님 등) 도 따라할 수 있도록 큰 글씨 + 단계별 안내.
User-Agent 자동 감지로 해당 기기 가이드 우선 표시, 탭으로 다른 기기 전환 가능.

* 안드로이드: Chrome → 앱 설치 배너 → 4단계
* 아이폰: Safari → 공유 → 홈 화면에 추가 → 5단계 (사파리 필수 경고 강조)
* PC: QR 코드 (휴대폰 카메라로 즉시 안내 페이지 이동)

모바일 로그인 화면 하단에 "📱 휴대폰 홈 화면에 앱처럼 설치하는 방법" 링크 추가.
middleware publicPaths 에 /install 추가 (비로그인 접근 허용).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 13:58:36 +09:00
chpark 7a712c164e feat(privacy): /account-deletion 페이지 — Play 데이터 보안 필수 URL
Deploy momo-erp / deploy (push) Successful in 2m4s
Google Play 데이터 보안 섹션에서 '계정 URL 삭제' 필드에 넣을 페이지.
앱 내 self-service 삭제가 없는 B2B 앱이므로 이메일/전화 요청 절차를
명시한 정적 안내 페이지로 처리.

* 삭제 요청 방법 (이메일/전화)
* 처리 기간 (14일 이내)
* 삭제되는 데이터 vs 법령상 보관되는 데이터
* /privacy 와 동일하게 middleware 인증 면제

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:09:01 +09:00
chpark 45af622afb chore(security): .gitignore — 안드로이드 서명 키/번들 차단
Deploy momo-erp / deploy (push) Successful in 1m55s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:18:40 +09:00
chpark 2419ded4ac fix(android): assetlinks.json — PWABuilder 가 발급한 실제 서명 키 SHA-256 으로 교체
Deploy momo-erp / deploy (push) Successful in 2m1s
PWABuilder 가 .aab 빌드 시 생성한 keystore 의 SHA-256 으로 갱신.
Play App Signing 활성화 후 Play 가 별도 서명 키를 발급하면 그 SHA-256 도
배열에 추가해 두 개 다 인정되도록 해야 함.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:15:29 +09:00
chpark b58d7e6821 feat(android): /.well-known/assetlinks.json — TWA Digital Asset Links
Deploy momo-erp / deploy (push) Successful in 2m1s
Play Store TWA(Trusted Web Activity) 풀스크린 동작에 필수. 안드로이드 앱
패키지 com.momotogether.app 의 서명 SHA-256 을 momotogether.com 도메인이
인정한다고 선언.

* 현재 SHA-256: 로컬 업로드용 키(android.keystore in ~/Downloads/momo-twa)
* Play App Signing 이 별도 서명 키를 발급하면 그 SHA-256 도 배열에 추가 필요

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:59:53 +09:00
chpark 80f490e8d6 feat(privacy): /privacy 공개 페이지 추가 — Play Store 등록용 개인정보 처리방침
Deploy momo-erp / deploy (push) Successful in 5m55s
- /privacy 라우트: 인증 미들웨어 면제. 시행일/수집항목/보유기간/제3자/안전성/연락처
- 추적·광고 SDK 없이 ERP 운영 데이터만 다루는 자체 서비스 기준으로 작성
- middleware publicPaths 에 /privacy 추가

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:41:58 +09:00
chpark 326b790e4a fix(inbounds): 발주요청/입고완료 발주도 입고 입력 가능
Deploy momo-erp / deploy (push) Successful in 1m52s
editable 조건을 PAID/PARTIAL 만 → REQUESTED/PARTIAL/PAID/RECEIVED 로 확장.
OPEN(작성중)/CANCELLED 만 차단. 입금 의존성은 이미 해제됐고, 이번 변경으로
'발주요청 상태라 입고 입력이 불가합니다' 차단 메시지 제거.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:55:53 +09:00
chpark 461164c397 feat(procurement): 입금/입고 순서 의존성 해제 — 발주요청 즉시 입고 가능, 입고완료 후 입금 가능
Deploy momo-erp / deploy (push) Successful in 1m53s
신규 업무 흐름: 매입발주 작성 → 발주요청 → (순서 무관) 입고 / 입금
- 발주요청(REQUESTED) 단계에서 입금 처리 없이 바로 입고 가능
- 입고완료(RECEIVED) / 입고중(PARTIAL) 건도 그 이후에 입금 처리 가능
- 이미 입금완료(PAID) 인 발주에 추가 입고가 들어와도 상태는 PAID 유지

변경 파일:
- proc-payments/confirm: 입금 허용 상태 REQUESTED → REQUESTED/PARTIAL/RECEIVED
- proc-payments/list: 노출 상태 (REQUESTED,PAID) → (REQUESTED,PARTIAL,RECEIVED,PAID)
- inbounds/page: 기본 필터 PAID_OR_PARTIAL → INBOUNDABLE (REQUESTED+PARTIAL+PAID)
  드롭다운에 '발주요청만' 옵션 추가, 안내 문구 갱신
- inbounds/save: 입고 후 상태 갱신 시 PAID 면 덮어쓰지 않음

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:50:33 +09:00
chpark 209b47c7f2 fix(admin): 거래명세표/발주서 캡처 영역 폰트 사이즈 +2px 상향
Deploy momo-erp / deploy (push) Successful in 1m55s
레이아웃 영향 없는 범위에서 가독성 ↑:
- text-[11px] → text-[13px]
- text-[12px] → text-[14px]
- 작은 배지/REMARK 등은 기존 비율 유지 (9→11, 10→12)

화면/이미지 공유/인쇄 동일하게 굵고 큰 텍스트로 나옴.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:40:45 +09:00
chpark dc05d48c82 fix(admin): 거래명세표/발주서 전체 bold + 매입발주서 좌:우 비율·품명폭 조정
Deploy momo-erp / deploy (push) Successful in 1m53s
- 거래명세표(admin/orders) 캡처 영역 전체 font-bold [&_*]:font-bold — 가독성 ↑
- 발주서(admin/procurements) 캡처 영역도 동일하게 bold 적용
  화면/이미지 공유/인쇄 모두 굵게 표시
- 매입 발주서 관리 좌:우 레이아웃: 360px → 480px (왼쪽 리스트 더 넓게)
- 발주서 품명 컬럼 width 220px 로 제한 (단위/수량/단가/금액 열에 공간 양보)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:32:24 +09:00
chpark 73317166ab feat(admin): 동적 리스트 select → SearchableSelect 로 통일 (결과내 검색)
Deploy momo-erp / deploy (push) Successful in 1m53s
긴 동적 리스트(거래처/창고/품목/공급업체/매입발주)의 native <select> 를
검색 가능한 SearchableSelect 로 교체. 항목 50~100건이라도 타이핑으로 즉시 필터.

대상:
- admin/orders 출고처리: 거래처 변경
- admin/inventory: 검색 창고 / 매입입고 모달(창고+품목) / 재고이동 모달(출발+도착+품목)
- admin/inbounds/new: 매입발주 / 공급업체 / 입고창고 / 품목
- admin/procurements/new: 품목
- admin/inventory/history: 창고 필터

상태/유형 등 짧은 고정 옵션 select 는 native 그대로 (드롭다운 클릭 즉시 선택이 빠름).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:29:26 +09:00
chpark 9e4d506939 fix(admin/orders): 좌:우 비율 2:3, 좌측 리스트 압축 + 거래명세표 품명 한 줄 유지
Deploy momo-erp / deploy (push) Successful in 1m56s
- 외곽 grid 50:50 → 2fr:3fr (좌 40 / 우 60). 거래명세표 패널 폭 확보.
- 좌측 발주 리스트: 패딩/폰트 축소(px-1.5, text-[11px]), 합계 whitespace-nowrap, 업체 max-w-[100px].
- 우측 거래명세표 품명 td: whitespace-nowrap — 2줄 줄바꿈 방지.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:20:43 +09:00
chpark adff1347c9 fix(admin): 관리자 패널 '← 사용자' 링크 404 — /dashboard → /m/orders/new
Deploy momo-erp / deploy (push) Successful in 6m25s
- admin-panel 헤더 '← 사용자' 링크가 삭제된 /dashboard 로 가서 404 발생
- 동일 패턴: login 페이지 fallback redirect 도 /m/orders/new 로 정정

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:04:49 +09:00
chpark 3c73c5a47a fix(admin/orders): 출고처리 — 세금계산서 발행 버튼 제거 + 기본 검색 상태 '출고요청+출고완료'
Deploy momo-erp / deploy (push) Successful in 2m2s
- 출고처리 우측 상세에서 [세금계산서 발행] 버튼 제거 (계산서(면세) 만 유지)
  업무 흐름: 출고요청 → 출고완료 → 입금완료 → 계산서 발행 → 전자세금계산서.
  입금완료 후의 발행은 계산서 관리 메뉴에서 처리.
- 검색조건 기본값: 'EDITABLE' (= REQUESTED + APPROVED) 두 상태만 노출.
  드롭다운에 '출고요청+출고완료' 옵션 추가, 기존 '전체 상태'/개별상태 옵션 유지.
- orders/list API: statuses?: string[] (IN) 파라미터 지원 (status 단일은 기존대로).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:59:44 +09:00
chpark 4a6a5fe6dc fix(orders/new): 판매기간 필터를 ADMIN 에도 적용 + 재고0 음영 표시 + 택배 품목은 재고무관 구매
Deploy momo-erp / deploy (push) Successful in 1m57s
- items/list: forSale=true 시 ADMIN 도 sale_start/end 필터 적용 (USER 는 기존대로 항상)
- orders/new: forSale=true 전송, onlyAvailable/재고필터 드롭다운 제거
- 재고 0 품목도 표시하되 opacity-50 음영 + "품절" 라벨
- 택배 전용(requires_delivery='Y') 품목은 음영 X, 재고 0이어도 담기 가능

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:27:08 +09:00
chpark f73c486c4f feat(items): 품목 판매 가능일 (sale_start/end) — 관리자 일괄 적용 + USER 필터
Deploy momo-erp / deploy (push) Successful in 1m55s
- momo_items: sale_start_date/sale_end_date DATE 컬럼 (ensureColumns)
- USER 측 출고요청: CURRENT_DATE ∈ [start, end] 인 품목만 노출 (NULL=상시)
- 택배 전용(requires_delivery='Y') 품목은 재고 무관 노출 (onlyAvailable)
- 관리자 품목 관리: 체크박스 + 시작일/종료일 일괄 적용 바, 모달에 판매기간 입력, 리스트에 판매기간 컬럼

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:46:33 +09:00
chpark 86b90e2d5a fix(capture): date input 캡쳐 시 빈 표시 → 같은 값의 span 프록시로 대체
Deploy momo-erp / deploy (push) Successful in 2m12s
2026-05-15 09:35:01 +09:00
chpark d36d256f27 fix(admin lists): 검색 조건 변경 시 자동 갱신 (실시간 필터링)
Deploy momo-erp / deploy (push) Successful in 1m58s
내 출고 이력처럼 검색 조건 변경 즉시 리스트 갱신 — 조회 버튼 불필요.
useEffect 의존성을 빈 배열 → [load] (load 의 deps 에 검색 조건 포함)
패턴으로 통일.

- admin/orders: status/dateFrom/dateTo/keyword 변경 시 즉시
- admin/payments: dateFrom/dateTo/keyword/payFilter
- admin/inventory: whFilter/keyword (load 를 useCallback 으로 wrap)

기타 페이지(invoices/inbounds/procurements/proc-payments/einvoices) 는
이미 [load] 패턴으로 자동 갱신 적용된 상태.
2026-05-15 02:40:47 +09:00
chpark 0e041676b7 fix(capture): 캡쳐 후 발주일자 input 사라지는 버그 — 처리 순서 역순 fix
Deploy momo-erp / deploy (push) Successful in 1m57s
root cause: .js-no-export 인 input 의 cssText 캡쳐 시점에 이미
display:none 이 적용되어 있어, finally 의 cssText 원복이 display:none 을
다시 적용 → input 이 영구히 사라짐.

수정:
- 진입 순서: input cssText 캡쳐 FIRST → 그 다음 .js-no-export display:none
- 원복 순서: cssText FIRST → 그 다음 display 원복 (역순)
- 이로써 cssText 의 'display:none' 잔류 방지

부수: admin/orders 의 '출고' 버튼은 출고완료(APPROVED) 발주에서 숨김 —
'editable && (출고)' → 'STATUS === REQUESTED && (출고)'.
2026-05-15 02:35:18 +09:00
chpark 0139282231 fix(capture+loading): 이미지 캡쳐 후 input 사라짐 fix + 처리 중 로딩 오버레이
Deploy momo-erp / deploy (push) Successful in 1m56s
1) capture-share: display:none 으로 빈 input 숨기던 처리 제거.
   외형(border/bg/padding) 만 잠시 제거하고 cssText 로 원복. 캡쳐 후
   날짜 input 이 사라지던 문제 fix (React 가 같은 DOM 재사용 시 hidden
   상태로 남던 케이스 회피).
2) 처리 중 로딩 오버레이 — components/ui/Loading 컴포넌트 활용:
   · admin/orders 페이지 (busy = 일괄 출고)
   · admin/orders StatementPreview (shipping = 단건 출고)
   · admin/inbounds (busy = 입고 저장)
   · admin/proc-payments (busy = 입금 처리)
   가운데 spinner + 메시지 + 반투명 black overlay.
2026-05-15 02:29:44 +09:00
chpark aeafeb9daf fix(capture-share): 이미지 캡쳐 시 input/select 외형 제거 + 발주번호 nowrap
Deploy momo-erp / deploy (push) Successful in 1m59s
문제:
- 거래명세표 이미지 공유 시 input 박스(테두리/배경/그림자) 가 그대로 노출
- 발주번호 'O2605140012'/발주일자 텍스트가 좁은 폭에서 wrap 되어 깨짐
- 비고 빈 인풋이 영역만 차지

수정:
- capture-share: 캡쳐 직전 모든 input/select/textarea 의 border/bg/
  shadow/padding 제거. select 는 appearance:none 으로 화살표도 가림.
  값 비어있는 input 은 display:none. 캡쳐 후 cssText 로 일괄 복원.
- admin/orders: 발주번호/발주일자 영역 whitespace-nowrap + flex 로 변경.
2026-05-15 02:17:18 +09:00
chpark 29641ed978 fix(orders): 환불 라인 UPDATE — WHERE kind IN 에 REFUND 추가 (단가 저장 실패 fix)
Deploy momo-erp / deploy (push) Successful in 1m54s
2026-05-15 02:10:56 +09:00
chpark 373e1962f0 fix(orders): DetailLine KIND 타입에 REFUND 추가 — 빌드 fail fix
Deploy momo-erp / deploy (push) Successful in 1m58s
2026-05-15 02:05:16 +09:00
chpark b7c7a4d395 feat(orders): admin 출고관리 — 환불(REFUND) 라인 추가
Deploy momo-erp / deploy (push) Failing after 47s
요청: 출고관리 detail 에 '환불 추가' 버튼. 누르면 표 상단에 환불 라인
생성. 수량=1 고정, 단가 양수로 입력 시 내부에서 음수로 저장 → 총합
자동 차감.

- lines/save API: kind 'REFUND' 추가. admin 만 허용. 단가 양수 받아
  음수로 저장. is_tax_free='Y' (면세) 처리. seq=0 으로 박아 상단 정렬.
- detail/route.ts: ORDER BY 에 REFUND 우선 노출.
- admin/orders UI: '환불 추가' 버튼 (rose), ExtraRow 가 REFUND 도 처리
  (음수 표시 — 빨간 글씨, 수량 1 고정 잠금).
- USER /m/orders: items.filter(KIND !== 'REFUND') — 사용자 화면에서
  환불 라인 비공개. 합계는 DB 의 차감된 값 그대로 노출 (사용자에게
  최종 청구 금액 표시).
2026-05-15 02:01:45 +09:00
chpark c7d7bdfaea feat(orders): 거래명세서 발주일자 수정 — REQUESTED/APPROVED 만 (date input + 자동저장)
Deploy momo-erp / deploy (push) Successful in 1m56s
2026-05-15 01:50:21 +09:00
chpark d6b81da946 feat(orders+procurements): 발주번호 형식 간소화
Deploy momo-erp / deploy (push) Successful in 1m55s
ORD-20260514-0001 → O2605140001 (O + YY MM DD + 0001, 11자리)
PRC-20260514-0001 → P2605140001 (P + YY MM DD + 0001, 11자리)

- genOrderNo / genProcNo 4개 함수: prefix 변경 + ymd 6자리 (YY)
- LIKE prefix||'%' 패턴은 그대로 — 새 prefix 가 자동 적용됨
- 운영 DB 기존 데이터 일괄 UPDATE 완료 (orders 22건, procurements 4건)
2026-05-15 01:42:48 +09:00
chpark c9dea94bc2 chore(cleanup): FITO 레거시 (main) 폴더 일괄 삭제
Deploy momo-erp / deploy (push) Successful in 1m56s
menu_info 에 등록된 활성 URL prefix 는 m/* 와 admin/* 뿐 — 그 외 폴더는
옛 FITO/우성 레거시. 사용자 토글 시 진입되던 /dashboard 더미 페이지 포함
전부 정리.

삭제 폴더 (총 23개):
- dashboard (영업현황/제품별현황 더미)
- approval/bom/cost/cost-mgmt/cs/delivery/fund/inventory
- order/part/part-mgmt/procurement-std/product/product-mgmt
- production/project/purchase/purchase-order/quality/sales/scm/work

유지: m/, admin/, profile/. /api/admin/* 와 /api/approval 등 API 라우트는
admin-panel 의 일부 inline 컴포넌트가 참조하므로 그대로 둠.
tsc 타입 체크 통과.
2026-05-15 01:35:57 +09:00
chpark 7604027155 fix(admin lists): 리스트 행 높이 축소 + 품목관리 작업 셀 nowrap
Deploy momo-erp / deploy (push) Successful in 2m7s
- payments/invoices/items/proc-payments/einvoices 5개 페이지에서
  px-4 py-3 → px-3 py-1.5 일괄 축소. 한 화면에 더 많은 행 표시.
- items: 작업(Pencil/Trash) 셀에 whitespace-nowrap + w-[80px], 품목명
  컬럼 max-w-[260px] truncate (긴 이름은 title 으로 hover)
2026-05-15 01:28:54 +09:00
chpark 51f6bd653b fix(wh-stock-status): 기본 '품목 가로' + 상단 전체 합계 행 추가
Deploy momo-erp / deploy (push) Successful in 2m10s
2026-05-15 01:23:13 +09:00
chpark a02015641c fix(orders): admin 출고처리 — ITEM 라인 삭제 버튼 추가 (REQUESTED/APPROVED)
Deploy momo-erp / deploy (push) Successful in 2m7s
2026-05-15 01:19:10 +09:00
chpark a120803799 fix(inventory): 재고관리 매트릭스 — 품목 가로(기본) / 창고 가로 토글
Deploy momo-erp / deploy (push) Successful in 2m9s
2026-05-15 01:16:56 +09:00
chpark 71cf966781 fix(wh-stock-status): 기본 '품목 가로' + 상단 전체 합계 행 추가
Deploy momo-erp / deploy (push) Successful in 2m6s
- viewMode 기본값 'by-wh' → 'by-item' 으로 변경 (사용자 선호).
- 품목 가로 모드 본문 첫 행: '전체 합계' (emerald 강조)
  · 발주수량 합 = Σ 모든 창고의 STOCK
  · 여유분 합   = Σ 모든 창고의 AVAILABLE (음수면 rose)
- 이후 본사 창고 → 김포 시장 7줄 그대로 노출.
2026-05-15 01:04:07 +09:00
chpark 17ae2b80d7 fix(statistics): 월간/일자별 매출 — 좌측 리스트 / 우측 차트 50:50 레이아웃
Deploy momo-erp / deploy (push) Successful in 2m9s
2026-05-15 00:57:57 +09:00
chpark 08549146be fix(margin): 좌측 리스트 / 우측 차트 50:50 레이아웃 + 리스트 스크롤
Deploy momo-erp / deploy (push) Successful in 2m6s
2026-05-15 00:37:00 +09:00
chpark e37d6eaa13 fix(layout): 관리자 토글 시 admin-panel 페이지 이동 + sidebar 공급업체 항목 제거
Deploy momo-erp / deploy (push) Successful in 2m8s
- header: 관리자 토글 클릭 시 router.push('/admin-panel?tab=user'),
  사용자로 돌아갈 때 '/m/orders/new'. 사이드바만 바뀌고 콘텐츠는 그대로
  남아 혼동되던 UX 개선.
- sidebar ADMIN_SYSTEM_MENU 의 '공급업체 관리' 항목 제거 — admin-panel
  에서 이미 supply 탭 제거됐기 때문에 클릭 시 이상한 페이지로 가던 문제 fix.
  공급업체 관리는 m/admin/vendors 별도 메뉴 사용.
2026-05-15 00:34:24 +09:00
chpark ea21dced45 fix(orders): 수기 출고 — 라벨 변경 + 현재고 표시 + 삭제 + admin-panel 정리
Deploy momo-erp / deploy (push) Successful in 2m13s
1) '수기 발주' → '수기 출고' 라벨 변경 (버튼/타이틀).
2) detail STOCK_QTY: 거래처 default_wh_objid 분기 제거 → 항상 STOCK 류
   전체 합산 표시. customer=admin 또는 김포 시장 등 default 가 빈 창고일
   때 현재고 0 으로 표시되던 버그 fix. 실제 출고 차감은 approve 시
   default_wh_objid 또는 STOCK 첫 창고 기준 그대로.
3) /api/m/orders/delete (admin) — REQUESTED 상태 발주만 hard delete.
   수기 출고로 잘못 생성한 빈 발주 정리용. einvoice/items/orders 일괄.
4) 출고관리 detail (REQUESTED) 에 '삭제' 버튼 추가 — 반려 옆.
5) admin-panel 의 '공급업체관리' 메뉴 제거 (m/admin/vendors 별도 메뉴 사용).
2026-05-14 23:45:29 +09:00
chpark 470fa4884d feat(orders): 출고관리 발주 리스트 — 리스트/카드 보기 토글
Deploy momo-erp / deploy (push) Successful in 2m7s
2026-05-14 22:11:01 +09:00
chpark 280495d741 feat(stock): 창고별 재고 현황 — 보기 토글 (창고 가로 ↔ 품목 가로)
Deploy momo-erp / deploy (push) Successful in 2m17s
토글 버튼 2개로 표시 형식 전환:
- '창고 가로' (기본): 헤더=창고 7개, 행=품목 (가로로 김)
- '품목 가로': 헤더=품목, 행=창고 7줄 (오른쪽으로 길게, 좌측 sticky)

각 모드에서 동일하게 셀당 '발주수량(현재고) / 여유분' 2행. 여유분 음수면 rose 강조.
2026-05-14 22:09:19 +09:00
chpark fac0f0d83e feat(orders): admin 출고관리 인라인 수기 발주 + 품목 추가 + 거래처 변경
Deploy momo-erp / deploy (push) Successful in 2m19s
매입 발주서 작성 패턴처럼 출고관리 안에서 직접 빈 발주 → 거래처 → 품목 채워가는 흐름.

신규 API:
- /api/m/orders/create-empty (admin) — 빈 발주 INSERT
  · status='REQUESTED', customer 임시 admin, HQ 기본 supplier snapshot
- /api/m/orders/update-customer (admin) — 발주의 거래처 변경
  · 변경 시 새 거래처 statement_branch 기반 supplier snapshot 재계산
  · REQUESTED/APPROVED 만 변경 허용 (입금 후 잠금)

UI (/m/admin/orders):
- '수기 발주' 버튼 → 즉시 create-empty 호출 → 리스트 새로고침 + 새 row
  자동 활성화 (모달/redirect 제거)
- detail 의 거래명세서 안 '귀하' 줄 → editable 시 CustomerEditor (select)
- 액션바에 '+ 품목 추가' 버튼 → AdminItemPickerModal (재고 있는 품목 검색)
  · items/add API 호출, ITEM 라인 일괄 INSERT
2026-05-14 22:07:45 +09:00
chpark 527cfddc1b feat(stock): 창고별 재고 현황 메뉴 (출고/정산 > 창고별 재고 현황)
Deploy momo-erp / deploy (push) Successful in 2m7s
매트릭스 뷰 — 행: 품목, 열: 창고 7개, 셀: 발주수량/여유분 2행.

- 발주수량 = 현재 보유 재고 (momo_stocks.qty)
- 여유분  = 현재고 - 기간 내 발주(REQUESTED/APPROVED/INVOICED/PAID)
           의 출고 예정 수량 (거래처 default 창고 기준)
- 기간 필터: dateFrom/dateTo (기본 이번 주 월 ~ 오늘) + '금주' 버튼
- 엑셀 다운로드 지원
- 여유분 음수면 rose 강조 (재고 부족 경고)

운영 DB menu_info 9000420 등록 (parent 9000400 출고/정산).
2026-05-14 16:47:03 +09:00
chpark 789909991a feat(orders): USER 측 품목 추가 + 택배/용차 직접 수정
Deploy momo-erp / deploy (push) Successful in 2m5s
USER 가 본인의 출고요청/출고완료 발주에 직접:
- 신규 품목 추가 (피커 모달 — /api/m/items/list 재고 있는 품목 검색)
- 택배비 단가/수량 인라인 수정 + 라인 삭제
- 용차 단가/수량 인라인 수정 + 라인 삭제

신규 API:
- /api/m/orders/items/add — ITEM 추가 (재고/숨김/한도 검증, admin 우회)
- /api/m/orders/lines/save — 가드 풀어서 REQUESTED + APPROVED 둘 다 허용

UI:
- detail modal 상단 액션 바: '+ 품목 추가' / '택배 추가/+1' / '용차 추가/+1'
- 표 안의 택배/용차 행에 수량/단가 QtyInput → onBlur 자동저장
- ItemPickerModal — 키워드 검색 + 행별 수량 입력 → 일괄 추가
2026-05-14 16:17:19 +09:00
chpark d25db4a023 feat(orders): admin 수기 발주 작성 — 거래처 대신 명의로 등록
Deploy momo-erp / deploy (push) Successful in 2m26s
전화 요청 등 시 admin 이 거래처를 대신해 발주를 작성할 수 있도록.

- /m/admin/orders 헤더에 '수기 발주' 버튼 + SearchableSelect 거래처 picker
  → 선택 후 /m/orders/new?customerObjid=momoNNN 로 이동
- /m/orders/new 가 query param customerObjid 받음:
  · admin 일 때만 활성 (USER 가 query 박아도 무시)
  · 상단 배너에 거래처명 표시 + 취소 링크
  · save 호출 시 body 에 customerObjid 포함
- /api/m/orders/save: admin 이 body.customerObjid 명시하면 그걸로
  발주 INSERT (supplier_branch snapshot 도 해당 거래처 기준)
2026-05-14 16:10:05 +09:00
chpark 8d8bb17345 feat(statistics+admin-panel): 통계 4개 거래명세서 필터 + 거래명세서 관리 메뉴
Deploy momo-erp / deploy (push) Failing after 4m39s
통계 페이지 4개에 거래명세서 기준(전체/본사/김포) 필터 추가:
- /m/admin/statistics (월간 매출)
- /m/admin/statistics/daily (일자별)
- /m/admin/statistics/margin (원가/마진)
- /m/admin/statistics/pivot (거래처×일자)

각 API 의 WHERE 절에
  COALESCE(O.supplier_branch, U.statement_branch, 'HQ') = $N
추가. supplier_branch snapshot 우선, 옛 발주는 user_info.statement_branch
폴백. ALL/생략 시 전체.

admin-panel 권한 및 사용자 관리 섹션에 '거래명세서 관리' 항목 추가
— activeTab='statement-branches' 시 /m/admin/statement-branches iframe
으로 로드 (기존 페이지 재사용, 별도 컴포넌트 중복 없음).
2026-05-14 16:09:41 +09:00
chpark 3e2d8572f1 feat(orders): USER 측 출고요청 + 출고완료 모두 수정 가능 (입금완료 전까지)
Deploy momo-erp / deploy (push) Successful in 2m5s
- 사용자 detail modal editable: REQUESTED → REQUESTED + APPROVED 모두 허용
- items/update API USER 가드: 동일하게 REQUESTED + APPROVED 허용
- 안내 문구 상태별로 분기

품목 추가/택배비 수정은 다음 단계에서 작업.
2026-05-14 16:05:36 +09:00
chpark 7977ffff19 fix(branch-fee): 본사(HQ) 제외 + 계산서 발행 명의(supplier_branch) 기준
Deploy momo-erp / deploy (push) Successful in 2m6s
지사관리:
- WHERE COALESCE(supplier_branch, statement_branch, 'HQ') != 'HQ'
  → 본사 발주 완전 제외, 김포 등 지사 명의 계산서만
- snapshot 우선 (supplier_branch) → 옛 발주 폴백 (user_info.statement_branch)
- UI: 본사 row 표시 분기 제거, '본사 외' 명시

창고이동 통계:
- 본사 계열(HQ_*) → 김포 계열(KIMPO_*) 이동만 필터링
- 같은 본사 내 이동, 같은 김포 내 이동, 김포→본사 역방향 모두 제외
2026-05-14 15:08:18 +09:00
chpark bb21be260f fix(branch-fee): 본사(HQ) 제외 + 계산서 발행 명의(supplier_branch) 기준
Deploy momo-erp / deploy (push) Successful in 2m11s
사용자 요구 — 지사 수수료 페이지는 본사 거래처를 보여줄 필요 없음.
계산서가 김포 등 지사 명의로 발행된 발주만 표시.

- 그룹핑 기준: COALESCE(supplier_branch, user.statement_branch, 'HQ')
  · 발주 시점의 supplier_branch snapshot 우선 (= 계산서 발행 명의)
  · 옛 발주(snapshot 없음) 는 거래처 statement_branch 폴백
- WHERE branch != 'HQ' 로 본사 제외
- UI: 본사 분기 제거 (모든 행이 지사)
2026-05-14 15:05:10 +09:00
chpark 7b5951c227 feat(alerts+transfers): 유통기한 임박 알림 + 창고이동 통계 메뉴
Deploy momo-erp / deploy (push) Successful in 2m8s
A. 유통기한 임박 알림 (/m/admin/expiry-alerts)
   - momo_inbounds.expiry_date/completed_by 컬럼 운영 DB 추가
   - inbounds/save API: 입고 시 expiryDate/completedBy 함께 저장
   - 페이지: 만료/7일이내/30일이내 분류 카드 + 행별 D-N 뱃지
   - 빠른 필터 (7/14/30/60/90일)

B. 창고 이동 통계 (/m/admin/transfers)
   - stock_moves WHERE ref_type='TRANSFER' AND move_type='OUT' 기준
   - 출발창고 / 도착창고 / 품목 / 수량 / 단가(cost_price) / 금액
   - 이동자(regid + user_name), 이동일시, 메모
   - 합계 카드 + 엑셀 다운로드

운영 DB:
- ALTER TABLE momo_inbounds ADD COLUMN expiry_date, completed_by
- 인덱스 idx_momo_inbounds_expiry
- menu_info: 9000511 (유통기한), 9000512 (창고이동) 등록 — 통계 메뉴 산하
2026-05-14 14:58:58 +09:00
chpark 9fd1160b38 feat(branch-fee): 지사 수수료 관리 메뉴 — 본사 20% 수수료 산정
Deploy momo-erp / deploy (push) Successful in 2m9s
신규 메뉴 /m/admin/branch-fee — 거래처의 statement_branch(HQ/KIMPO 등)
기준으로 매출/원가/순수 마진 그룹핑 + 지사(HQ 외) 의 마진 × 20% =
본사 수수료 자동 계산.

표시:
- 합계 카드 4개: 총 매출 / 총 마진 / 본사 수수료 합 / 지사 실수령 합
- 지사별 표: 매출/원가/마진/수수료/실수령(마진-수수료)
- 본사(HQ) 행은 수수료 0 (— 표시)

집계 범위: 출고완료(APPROVED)/계산서발행(INVOICED)/입금완료(PAID) 발주.
운영 DB 의 menu_info objid=9000510 으로 등록 완료. (parent 9000500 통계)
2026-05-14 14:49:43 +09:00
chpark b568a8858a feat(procurements+inbounds): 발주서 납품조건 1번 삭제 + 입고 체크리스트
Deploy momo-erp / deploy (push) Successful in 2m16s
매입 발주서 납품조건:
- '상기 품목의 납기 지연 시...' 1번 항목 삭제. 검수/수량변경 2개만 유지.

입고 처리 체크리스트 (memo 컬럼에 텍스트로 저장 — 스키마 변경 없음):
1) 발주수량 vs 입고수량 일치 (체크박스)
2) 1카톤 N개 일치 (체크박스 + 카톤 단위 입력)
3) 소비기한 (date)
4) 물류창고 입고 최종완료자 (물류팀 4명 select)
5) 특이사항 (textarea)

저장 시 momo_inbounds.memo 에 줄단위 텍스트로 박힘:
  [수량 일치]   Y ✓
  [카톤 일치]   Y ✓ (1카톤 12개)
  [소비기한]    2026-11-30
  [입고완료자]  이효철 (물류총괄)
  [특이사항]    ...
2026-05-14 14:43:03 +09:00
chpark 3505148994 feat(orders): 출고완료 상태 수량 변경 시 재고 ± 동기화 + 이력
Deploy momo-erp / deploy (push) Successful in 2m6s
- 직전엔 items/update 가 momo_order_items.qty 만 UPDATE → 출고완료
  발주의 수량을 줄여도 재고는 그대로 (출고 시 차감된 양 그대로 묶임).
- 수정: status='APPROVED' 인 경우 newQty - oldQty 차이만큼 재고 보정
  · diff > 0 (추가 출고) → stock 차감 + stock_moves OUT
  · diff < 0 (수량 줄임)  → stock 복원 + stock_moves IN
- 사용된 창고는 기존 stock_moves(ref_type='ORDER', ref_objid=order, 동일
  item) 의 wh_objid 로 lookup (approve 시 사용했던 창고와 동일 유지).
- 이력 memo: "수량 수정: oldQty → newQty"
2026-05-14 01:33:53 +09:00
chpark 88686d0461 fix(orders): '내 발주 이력' 은 admin 이라도 본인 발주만 (mine=true)
Deploy momo-erp / deploy (push) Successful in 2m6s
momo5315(배연진) 같은 admin 임직원이 사용자 측 '내 발주 이력' 페이지를
열면 모든 발주가 노출되던 문제. admin 판정만으로는 부족 — 메뉴 의도가
'본인 것' 이라 isAdmin 여부 무관 customer_objid 본인 매칭 필요.

- list API: body.mine === true 면 admin 이어도 본인 발주만
- /m/orders/page.tsx fetch 에 mine: true 추가
- admin 메뉴(/m/admin/*)는 mine 안 보냄 → 기존대로 전체 노출
2026-05-14 01:31:07 +09:00
chpark 13d02cac6a chore(migrations): db/migrations 전체 삭제 + deploy 마이그레이션 단계 제거
Deploy momo-erp / deploy (push) Successful in 2m3s
사용자 명시 요청 — 운영 DB 는 이미 모든 스키마/데이터 변경 반영됐고,
더 이상 자동 reload 가 필요 없음. 매 deploy 시 마이그레이션이 실행되며
사용자 변경(비밀번호, 부서, 추가 거래처 등) 을 원복하는 사고를 막기 위해
폴더 통째로 삭제.

- db/migrations/*.sql 35개 모두 삭제
- .gitea/workflows/deploy.yml 의 migrate-momo.mjs 호출 단계 제거
- scripts/migrate-momo.mjs 파일 자체는 유지

운영 DB 의 모든 user_info 비밀번호는 '1' 로 직접 reset 완료(142명).
2026-05-14 01:26:30 +09:00
chpark f29024744d fix(orders): 비고 수정 = REQUESTED + APPROVED 두 상태 모두 허용 (출고완료 포함)
Deploy momo-erp / deploy (push) Successful in 2m34s
2026-05-14 01:22:11 +09:00
chpark e2c5c5b396 fix(orders): 내 발주 이력 — FITO 세션도 본인 발주만 보이도록
Deploy momo-erp / deploy (push) Successful in 2m39s
root cause:
- FITO auth.ts 는 user 객체에 role 필드를 만들지 않음 (isAdmin/userType 만).
- 새봄-마켓소풍(momo125) 가 FITO 폴백 경로로 로그인 → session.user.role
  = undefined → list API 의 `if (role === "USER")` 가 false → 필터 무효
  → admin 의 발주까지 다 노출.

→ admin 판정을 MOMO/FITO 공통 isAdmin/role/userType 세 조합으로 통일.
   !isAdmin 인 경우 본인 발주만 매칭.
2026-05-14 01:16:23 +09:00
chpark f4b1e31f7f fix(orders): 매출 출고 수정 = REQUESTED/APPROVED 만 (계산서발행/입금완료 차단)
Deploy momo-erp / deploy (push) Successful in 2m38s
2026-05-14 01:12:46 +09:00
chpark 5294554384 fix(orders): admin 그룹은 출고 발주 모든 상태 수량 수정 가능
Deploy momo-erp / deploy (push) Successful in 2m39s
momo5315(배연진) 같은 user_type='A' admin 이 출고요청/출고완료 발주
수량 수정이 안 된다는 사용자 신고. 직전 commit 에서 'REQUESTED/APPROVED'
만 허용으로 좁혔던 게 너무 빡빡했음.

→ admin 권한자는 취소(CANCELED) 외 모든 상태 수정 가능으로 풀기.
  · API items/update admin 분기: status === 'CANCELED' 만 차단
  · UI editable: order.STATUS !== 'CANCELED'
USER 권한자는 그대로 REQUESTED 만.
2026-05-14 01:08:29 +09:00
chpark b204f14265 fix(orders): 내 출고 이력에서 본인 발주만 — user.objid undefined 폴백
Deploy momo-erp / deploy (push) Successful in 2m37s
USER 권한 사용자의 list API 필터링에서 r.user.objid 가 undefined 인
세션에선 customer_objid 비교가 NULL 매칭 → 필터링 무효화돼 모든 발주가
노출되던 버그. user_id 폴백 + customer_objid 가 user_id 로 박힌 경우
모두 IN 절로 매칭.
2026-05-14 01:03:43 +09:00
chpark 34ee374796 feat(orders): ITEM 수량/비고 인라인 즉시저장 + 왼쪽 리스트 실시간 갱신
Deploy momo-erp / deploy (push) Successful in 2m35s
1) ITEM 라인 수량을 QtyInput 인라인 인풋으로 (REQUESTED/APPROVED 상태).
   onBlur/Enter 시 /api/m/orders/items/update 호출 → 자동 저장.
2) 비고(REMARK) 는 이미 onBlur 자동 저장이었음. 출고요청/출고완료 모두
   editable 이라 동작.
3) ExtraRow(택배/용차) 의 V(저장) 버튼 제거. onBlur 시 자동 저장으로 변경
   — label/단가/수량 어느 인풋이든 포커스 떠나면 자동 commit.
4) 모든 라인 수정 작업 (saveRemark/saveItemQty/upsertExtra/deleteExtra)
   에서 onReload + onReloadList 동시 호출 → 왼쪽 발주 리스트의 합계도
   즉시 반영.

부수: 운영 DB 의 모든 user_info 비밀번호를 '1' 로 reset (사용자 요청).
2026-05-14 01:01:06 +09:00
chpark 053a21c30e fix(invoices): 계산서 발행 대상 = 입금완료(PAID) 만
Deploy momo-erp / deploy (push) Successful in 2m42s
리스트:
- 기본 표시 = PAID + INVOICED 만 (APPROVED 출고완료 제외)
- 상태 필터 옵션도 PAID/INVOICED 만

발행 가드:
- frontend: 선택 + PAID + 미발행만 issue 가능
- API: UPDATE 조건 status='PAID' (이전엔 APPROVED 도 허용했음)
2026-05-14 00:45:39 +09:00
chpark 665a560486 fix(orders): 출고관리 수정 범위 좁히기 — REQUESTED + APPROVED 만
Deploy momo-erp / deploy (push) Successful in 2m52s
직전엔 PAID/CANCELED 외 모두 허용했는데, 사용자 재요청으로 계산서발행
(INVOICED) 도 잠금. editable = REQUESTED || APPROVED.
items/update API admin 가드도 동일하게 좁힘.
2026-05-14 00:43:22 +09:00
chpark 3a7d17b3e5 feat(orders): 출고 처리 기본 날짜 한 달 전 ~ 오늘로 변경
Deploy momo-erp / deploy (push) Successful in 2m50s
2026-05-14 00:42:02 +09:00
chpark 2d5b94a026 feat(admin/users): 수정 화면에서도 비밀번호 변경 가능
Deploy momo-erp / deploy (push) Successful in 2m36s
- 사용자 수정 폼에 '비밀번호 변경' 입력란 추가 (빈 칸이면 기존 유지)
- /api/admin/users/save update 분기: password 값이 있으면 AES 암호화 후
  user_password 갱신
2026-05-14 00:16:07 +09:00
chpark 9a086dae50 fix(migrations): momo001..momo135 부서 '일반구매자' (DEPT003) 매 deploy 보장
Deploy momo-erp / deploy (push) Successful in 2m42s
운영 DB 에는 직접 UPDATE 로 부서 설정 완료. 035 도 INSERT/UPSERT 시
dept_code='DEPT003', dept_name='일반구매자' 박도록 패치.
임직원 6명(user_type='A') 은 영향 없음.
2026-05-14 00:10:19 +09:00
chpark 4661981da5 feat(procurements): 입고완료 이후 수정 잠금 + 공급업체 SearchableSelect
Deploy momo-erp / deploy (push) Successful in 2m54s
- 매입 발주서 editable 재조정: OPEN/REQUESTED 만 수정 가능. RECEIVED/
  PARTIAL/PAID/CANCELLED 는 수정 불가. lines/save API 가드도 동일.
- 공급업체 select 를 SearchableSelect 로 교체 (typeahead):
  · 매입 발주서 관리 페이지의 발주서 폼 (page.tsx)
  · 매입 발주 신규 작성 페이지 (new/page.tsx)

운영 DB 의 매입 발주/입고/관련 stock_moves 데이터는 직접 모두 삭제했음
(사용자 명시 요청). UI 에 깨끗한 상태로 보임.
2026-05-14 00:08:27 +09:00
chpark d86a1154a9 fix(migrations+procurements): supply_mng objid numeric + setActiveId 빌드 에러
Deploy momo-erp / deploy (push) Successful in 2m34s
운영 DB 직접 점검 후 발견:
- supply_mng.objid 는 numeric NOT NULL (TEXT 아님) → 'MOMOSUP000000001'
  같은 문자열 INSERT 가 cast fail 로 80개 다 fail 하고 supply_mng = 0개
  잔존 상태였음. 운영 DB 에는 직접 80개 박아둠 (objid 1..80).
- 035 마이그레이션도 동일 패턴(numeric objid)으로 재작성. 매 deploy 안전.

직전 deploy #137 (485aea4) 빌드 실패 원인:
- procurements/page.tsx 의 deleteProc 에서 setActiveId(null) → state 가
  string 이라 타입 에러. setActiveId("") 로 수정.
2026-05-14 00:02:28 +09:00
chpark 6ea1f13003 fix(migrations): 031/032/033 NO-OP 화 + 035 plain SQL 단일 책임으로 통합
Deploy momo-erp / deploy (push) Failing after 58s
직전 batch 가 운영 반영 후에도 supply_mng 가 10개로 유지된다는 사용자 신고.
원인 추정: 031/032/033 의 DO block 안에서 어딘가 fail → migrate-momo.mjs 가
process.exit(1) → 그 뒤 마이그레이션 (034, 035) 실행 자체 못 함.

→ 031/032/033 본문을 SELECT 1; 로 NO-OP 화. fail 지점 우회.
→ 035 가 단독으로 회원 135 + 공급업체 80 + 제조사 메뉴 삭제 모두 처리.
  · DO block 없이 plain SQL 만 (각 statement 가 별도 transaction)
  · supply_mng: 통째 DELETE 후 80개 plain INSERT
  · user_info: user_type='U' DELETE 후 ON CONFLICT (user_id) UPSERT
  · 매 deploy 안전 — UPSERT 라 중복 INSERT 도 OK
2026-05-13 23:58:36 +09:00
chpark 485aea4d4f feat(procurements): 작성중 삭제 + 입금완료전 수정 + 035 plain SQL reload
Deploy momo-erp / deploy (push) Failing after 58s
- 매입 발주서: OPEN 일 때 삭제 버튼 노출, 라인 포함 hard delete
  · 신규 API /api/m/procurements/delete (status='OPEN' 만 허용)
- 매입 발주 editable: STATUS === 'OPEN' → STATUS not in (PAID, CANCELLED)
  · lines/save API 가드도 동일 (PAID/CANCELLED 만 차단)
- 035_supply_mng_plain_reload.sql: 직전 DO block 마이그레이션이 어디서
  fail 하는지 추적 불가 → 가장 단순한 plain SQL 로 supply_mng 다 비우고
  엑셀 기준 80개 INSERT ON CONFLICT(supply_code) UPSERT
  · momo_items/momo_procurements 의 vendor_objid 모두 NULL 처리
  · UNIQUE INDEX 보장 후 INSERT — 매 deploy 안전
2026-05-13 23:53:32 +09:00
chpark 5fce695f09 fix(migrations): 019 의 supply_mng 10개 매 deploy reset 차단 + 034 UPSERT
Deploy momo-erp / deploy (push) Successful in 2m36s
진짜 root cause — 019_proc_terms.sql 이 매 deploy 시
  DELETE FROM supply_mng;
  INSERT INTO supply_mng ... 10개 시드 (VND-001 ~ VND-010)
를 실행해서 supply_mng 가 항상 10개로 reset 되고 있었음.

→ 019 의 supply_mng 시드 부분 제거 (납품조건 ALTER 만 유지).

034 신설 — idempotent 매 deploy 안전 실행:
- product_lines 컬럼 보장
- supply_code UNIQUE 인덱스 보장 (ON CONFLICT 동작)
- 옛 'VND-*' 시드는 items/procurements 의 vendor_objid 끊은 후 DELETE
- 엑셀 80개 INSERT ON CONFLICT (supply_code) DO UPDATE
- sentinel 가드 없이 매번 안전 — 사용자 추가 supply (다른 supply_code) 는 보존
2026-05-13 23:46:30 +09:00
chpark 2d2f32f4f8 fix(auth-panel): 권한 제거 silent fail + 버튼 클릭 영역 확대
Deploy momo-erp / deploy (push) Successful in 2m36s
1) delete API: MASTER_OBJID/OBJID 가 numeric 가 아닐 때 ::numeric cast 가
   매칭 0건으로 silent fail. ::text 양쪽 비교로 안전하게.
2) 추가/제거 버튼: h-9 w-24 → h-12 w-32, cursor-pointer + hover 색상
   강화. 가운데 컬럼 min-w-[120px] 로 클릭 영역 넓힘.
2026-05-13 23:41:22 +09:00
chpark 1fde88bcd8 fix(migrations): 008/023 매 deploy 원복 사고 차단 + 033 강제 리로드
Deploy momo-erp / deploy (push) Successful in 2m49s
근본 원인:
- 008_makers_menu.sql: 매 deploy 시 제조사 관리 메뉴를 status='active' 로
  되돌려놓아 사용자가 메뉴 삭제해도 다음 배포 때 부활.
- 023_seed_momo_vendors_from_xlsx.sql: 매 deploy 시 옛 100개 공급업체를
  비-idempotent 한 INSERT 로 박아 supply_mng 가 영원히 옛 데이터로 원복.

→ 두 파일을 빈 NO-OP 으로 교체.

033_force_reload_v2.sql 신설 (sentinel 가드 momo_migration_marks 기반):
- 제조사 메뉴 menu_info objid=9000204 삭제
- momo_einvoice_items/einvoices/stock_moves(ORDER)/order_items/orders 통째
- user_info user_type='U' 다 삭제 후 momo001..momo135 ON CONFLICT UPSERT
  · 본사팀 85 (HQ), 김포팀 50 (KIMPO), 카테고리별 default_wh 매핑, 비번 '1'
- supply_mng 다 삭제 후 80개 INSERT (엑셀 모모유통 제조사 리스트 26.05.12)
  · product_lines 컬럼 ALTER (DO block 밖)
2026-05-13 23:37:40 +09:00
chpark 3d5a283955 feat(orders): 입금완료 전까지 거래명세표 수정 허용
Deploy momo-erp / deploy (push) Successful in 2m46s
사용자 요청 — 구매자가 수량 변경 요청하면 담당자가 거래명세표에서 수기로
수정하고 다시 입금 요청하는 흐름. 출고요청(REQUESTED) 뿐만 아니라
출고완료(APPROVED) / 계산서발행(INVOICED) 까지 수정 가능해야 함.

- editable: order.STATUS === 'REQUESTED' → STATUS !== 'PAID' && !== 'CANCELED'
- items/update API 의 admin 가드는 이미 PAID/CANCELED 외 모두 허용
- 수정하면 사용자 측 명세표도 그대로 갱신 (detail API 가 동일 데이터 반환)
2026-05-13 23:31:59 +09:00
chpark 91313351f9 chore(data): 회원/공급업체 마스터 통째 리로드 (마이그레이션 032)
Deploy momo-erp / deploy (push) Successful in 2m33s
사용자 요청 — docs 의 엑셀 두 파일을 기준으로:
- 회원(user_info user_type='U') 전부 삭제 후 momo001..momo135 신규 등록
  · 본사팀(85): 창고픽업 WH001 / 시장픽업 WH002 / 용차배송 WH003
  · 김포팀(50): 김포지사 WH004 / 창고픽업 WH005 / 시장픽업 WH007 / 용차배송 WH006
  · statement_branch: 본사='HQ' / 김포='KIMPO'
  · 비번 '1' (AES 암호화)
- 공급업체(supply_mng) 전부 삭제 후 80개 신규 등록 (제조사 리스트 엑셀 기준)
  · supply_mng 에 product_lines TEXT 컬럼 추가하여 제품명 보관
  · items.vendor_objid 는 NULL 처리 (참조 무결성)

1회성 sentinel — momo_migration_marks 테이블로 가드. 신규 데이터가
다음 deploy 때 또 지워지는 사고 방지.
2026-05-13 23:29:57 +09:00
chpark 94fc425ef3 chore(orders): 옛 발주 데이터 1회성 삭제 (마이그레이션 031)
Deploy momo-erp / deploy (push) Successful in 2m19s
사용자 명시 요청 — supplier snapshot 이전의 옛 발주 데이터를 통째로
비우고 신규 발주만 운용. 사용자가 새로 등록하기로 함.

⚠️ migrate-momo.mjs 가 매 deploy 시 모든 .sql 을 재실행하므로 raw DELETE
를 그대로 박으면 신규 발주가 다음 deploy 때 또 지워지는 사고가 남.
→ sentinel 테이블 momo_migration_marks 도입, 1회만 실행되도록 가드.

삭제 범위:
- momo_einvoice_items (발주연결분)
- momo_einvoices WHERE order_objid IS NOT NULL
- momo_stock_moves WHERE ref_type='ORDER'
- momo_order_items 전체
- momo_orders 전체

보존: momo_stocks.qty — 사용자가 inventory 메뉴에서 직접 보정.
2026-05-13 17:44:03 +09:00
chpark 0bfe85dc69 fix(orders): 거래명세표 공급자 정보 발주 시점 snapshot 처리
Deploy momo-erp / deploy (push) Successful in 3m37s
기존: 거래명세표 발급 때마다 user_info.statement_branch + branches table
을 실시간 조회 → 사용자의 기준 명세표를 바꾸거나, branches 의 계좌/
전화/이메일을 수정하면 과거 이미 찍힌 명세표까지 함께 바뀌어버림.

수정: 출고요청(REQUESTED) 시점에 supplier 8개 컬럼을 momo_orders 행에
박아두고, 이후 detail/statement/approve 는 이 snapshot 을 사용.

- 마이그레이션 030: momo_orders 에 supplier_branch / supplier_name /
  supplier_ceo / supplier_bank_account / supplier_phone / supplier_email
  / supplier_biz_no / supplier_address 컬럼 추가 (idempotent IF NOT EXISTS)
- save: 발주 INSERT 시 getSupplierByBranch(user.statement_branch) 호출 결과
  를 그대로 박음
- detail/statement/approve: snapshot 컬럼이 있으면 그것을 사용, 없으면
  옛 발주용 폴백으로 user_info.statement_branch → branches table 조회
2026-05-13 16:39:40 +09:00
chpark 2fffc42575 fix(orders): 거래명세표 택배/용차 추가 버튼 복구 + 캡처에서만 제외
Deploy momo-erp / deploy (push) Successful in 5m53s
직전 commit 에서 버튼 자체를 통째로 지웠던 게 잘못. 출고요청 건은
admin 이 화면에서 택배/용차 라인을 추가/수정해야 함.

- 화면: 버튼 항상 보임 (editable 시)
- 이미지 공유 캡처: .js-no-export 클래스로 hide (capture-share.ts 동작)
2026-05-13 16:35:47 +09:00
chpark 29852110dc chore(admin): 공통코드 관리 메뉴/UI/API 일괄 제거
Deploy momo-erp / deploy (push) Successful in 2m52s
momo 영역에서 미사용. FITO 레거시 잔존 코드 정리:
- sidebar.tsx: __sys_code 가상 메뉴 항목 제거
- admin-panel/page.tsx: code 탭/메뉴/CodeManagement 함수 삭제
- admin-panel/code-form/: 폼 페이지 디렉토리 통째로 삭제
- api/admin/codes/: list/detail/save 라우트 통째로 삭제

/api/common/code-list (조회 전용) 는 product/part-change 등이
드롭다운 로드용으로 쓰고 있어 보존.
2026-05-13 16:29:48 +09:00
chpark 5778b845d1 fix(orders): 현재고 0 표시 버그 + 거래명세표 레이아웃 정리
Deploy momo-erp / deploy (push) Successful in 3m13s
- 마이그레이션 029 후 wh_type 이 새 enum(HQ_STOCK/KIMPO_STOCK 등)
  으로 바뀌면서 detail/approve 의 wh_type='STOCK' 필터가 매칭 안 됨
  → 거래명세표 현재고가 전부 0 으로 표시됨 (재고 부족 N건 오탐).
- detail: 거래처 default_wh_objid 우선 + IN ('STOCK','HQ_STOCK','KIMPO_STOCK') 폴백
- approve: fallback 창고 lookup 도 동일하게 IN 절 확장
- 거래명세표 헤더 레이아웃: grid-cols-1 sm:grid-cols-2 → flex justify-between
  으로 변경, 발주번호 라인과 공급자 박스가 항상 가로 배치(공급자 오른쪽 정렬)
- 거래명세표에서 '+ 택배 추가 / + 용차 추가' 버튼 제거 (요청)
2026-05-13 16:26:01 +09:00
chpark 0c380d94a0 feat(warehouses 029): 창고 7개로 정비 — 기존 5개 라벨/타입 갱신 + 김포 용차/시장 2개 신규
Deploy momo-erp / deploy (push) Successful in 2m15s
기존 5개 (objid/wh_code 그대로 유지 — 재고 이동·입출고 데이터 보존):
- WH001 본사창고 → 본사 창고 (HQ_STOCK)
- WH002 시장픽업 → 본사 시장 (HQ_MARKET)
- WH003 용차배송 → 본사 용차 (HQ_CHARTER)
- WH004 창고픽업팀 → 김포지사 (KIMPO_BRANCH)
- WH005 김포창고 → 김포 창고 (KIMPO_STOCK)

신규 2개 (idempotent INSERT):
- WH006 김포 용차 (KIMPO_CHARTER)
- WH007 김포 시장 (KIMPO_MARKET)

총 7개. 사용자 요청한 본사 3 + 김포 4 구성 정확히 맞춤.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 16:08:45 +09:00
chpark 2b3c4acdae fix(deploy): momo-erp 재기동 후 traefik 라우터 캐시 자동 새로고침
Deploy momo-erp / deploy (push) Successful in 2m15s
증상: momo-erp container 가 force-recreate 될 때 traefik docker provider 가
새 컨테이너의 라벨을 재인식 못 해 momotogether.com 요청이 Gitea 라우터로
fallback 되는 사고 (사용자 화면에 Gitea 500 에러 페이지 노출).

deploy.yml 의 docker compose up 직후 `docker restart traefik` 1회 추가.
1~2초 다른 도메인 잠시 down 되지만 라우터 정합성 보장됨.

병행: cron watchdog (/usr/local/bin/momo-traefik-watchdog.sh, 3분 간격)이
production HTML 의 "모모유통" 문자열 검증해 비정상 시 traefik 자동 재시작.
deploy 외 임의 시점 사고에도 최대 3분 안에 자동 복구.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:59:56 +09:00
chpark 19e3cf9048 feat(statement-branch-admin): 기준 명세표 관리 메뉴/페이지/API + 창고 카테고리 7개로 확장
Deploy momo-erp / deploy (push) Successful in 3m5s
마이그레이션 028:
- momo_statement_branches 테이블 신설 (code PK / name / bank_account / phone / email 등)
- HQ, KIMPO 기본 시드 INSERT (사용자가 관리자 페이지에서 편집 가능)
- 메뉴: 시스템 그룹에 '기준 명세표 관리' (M_ASBR / menu_info 9000310)

라이브러리 (src/lib/momo-branches.ts):
- 하드코딩 → DB 조회로 변경 (60초 in-memory 캐시)
- getSupplierByBranch 가 async — detail/statement/approve API 도 await 추가
- 저장 시 invalidateBranchCache() 호출

페이지/API (관리자 전용):
- /m/admin/statement-branches : list + 등록/수정/삭제 모달
- POST /api/m/admin/statement-branches/list
- POST /api/m/admin/statement-branches/save (regist / update / delete)

사용자 수정 폼:
- "기준 거래명세서" select 옵션이 하드코딩 본사/김포 → DB 의 branches list 동적 fetch

창고 관리:
- WH_TYPE 카테고리 5개 → 7개 (옛 enum 도 라벨 매핑은 유지)
  · HQ_STOCK 본사 창고
  · HQ_CHARTER 본사 용차
  · HQ_MARKET 본사 시장
  · KIMPO_BRANCH 김포지사
  · KIMPO_STOCK 김포 창고
  · KIMPO_CHARTER 김포 용차
  · KIMPO_MARKET 김포 시장
- 신규 추가 시 select 는 위 7개만 노출, 기존 데이터 (STOCK 등) 는 라벨로 자연 표시

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:48:08 +09:00
chpark 6ad57356a0 feat(statement-branch): 사용자별 거래명세서 공급자 정보 분기 (본사/김포)
Deploy momo-erp / deploy (push) Successful in 1m59s
마이그레이션 027:
- user_info 에 statement_branch VARCHAR(10) DEFAULT 'HQ' 컬럼 추가
- 기존 사용자 일괄 'HQ' 로 채움

라이브러리:
- src/lib/momo-branches.ts 신설 — HQ / KIMPO 두 branch 의 공급자 정보 정의
  · HQ: 기업은행 434-115361-01-016 (이상용) / 010-6369-8443 / momo8443@daum.net
  · KIMPO: 농협 351-1383-7634-13 (모모유통) / 010-5789-9431 / momokimpo@nate.com
- getSupplierByBranch(branch) helper

거래명세표 supplier 분기 (3개 API):
- /api/m/orders/detail: order.STATEMENT_BRANCH 따라 supplier 객체 결정
- /api/m/orders/statement/[id]: xlsx 다운로드도 동일
- /api/m/orders/approve: 메일 발송 stmt 도 동일

사용자 수정 폼:
- /api/admin/users/detail: statement_branch 반환 (default 'HQ')
- /api/admin/users/save: statement_branch 받아 UPDATE
- /admin-panel/user-form: "기준 거래명세서" select 추가 (본사/김포)

흐름: 거래처 사용자의 기준을 김포로 설정 → 그 사용자의 발주에 대한 거래명세표 supplier 가 김포 정보로 표시

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:29:02 +09:00
chpark 75f37d8eaf feat(filters+warehouse): 매입발주/입금관리 조건 한 줄 + 공급업체 SearchableSelect + 김포 창고 카테고리 추가
Deploy momo-erp / deploy (push) Successful in 2m8s
1) 매입 발주서 관리(/m/admin/procurements):
   - 조회조건 flex-wrap → flex-nowrap + overflow-x-auto (모바일 한 줄, 가로 스크롤)
   - 공급업체 select → SearchableSelect (타이핑으로 결과 내 검색)
   - 폰트/높이 축소 (h-9 → h-8, text-sm → text-xs)
2) 매입 입금관리(/m/admin/proc-payments): 동일 한 줄 정비
3) 창고 관리(/m/admin/warehouses):
   - WH_TYPE 에 KIMPO "김포 창고" 카테고리 추가 + 라벨/배지색 등록
   - 신규 창고 추가 시 select 옵션에 자동 노출

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:55:23 +09:00
chpark ec2e79d517 feat(filters): 매입발주·내발주이력·출고처리 조회조건 정비
Deploy momo-erp / deploy (push) Successful in 2m12s
- 매입 발주 관리(/m/admin/procurements):
  · 날짜 from~to + 공급업체 select 추가 (기본: 오늘 - 30일 ~ 오늘)
  · 조회 버튼 제거 — state 변경 시 자동 fetch
- 내 발주 이력(/m/orders):
  · 날짜 기본값을 오늘 - 30일 ~ 오늘로 설정 (URL 파라미터 우선 적용)
- 출고 처리(/m/admin/orders) 검색바:
  · 모바일에서 label 풀어진 5단 grid → 한 줄 flex-wrap 으로 압축 (h-8 + text-xs)
  · 모바일 폼이 화면을 너무 차지하던 문제 해소

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:42:39 +09:00
chpark 6b401071a4 fix(migration 026): menu_info 에 unique constraint 없어 ON CONFLICT 실패 → WHERE NOT EXISTS + UPDATE 분리
Deploy momo-erp / deploy (push) Successful in 2m10s
지난 commit 의 026 이 ON CONFLICT(objid) 로 INSERT 시도 → menu_info 에 PK/unique 없어 PG 에러.
패턴: INSERT WHERE NOT EXISTS + 그 다음 별도 UPDATE WHERE objid=... 로 idempotent 처리.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:25:48 +09:00
chpark 7d18285ac6 feat(menu+filter+history): 사이드바 메뉴 노출 + 자동조회 + 재고이력 한글/이동 상대창고 표시
Deploy momo-erp / deploy (push) Successful in 2m53s
1) 사이드바 메뉴 누락 fix (마이그레이션 026):
   - FITO menu_info 테이블에 9000304 '매입 입금관리', 9000305 '재고이력' INSERT
   - 기존 입고/재고 seq 재정렬 (11→12, 12→13)
   - momo_menus 만으로는 사이드바에 안 나옴 — menu_info 가 사이드바의 진짜 소스

2) 재고이력 표시 개선:
   - inventory/history API: REF_TYPE_LABEL (한글) + COUNTER_WH_NAME (이동 시 상대 창고) 추가
   - inventory/transfer 라우트: stock_moves 의 ref_objid 에 상대 창고 objid 박음
   - StockHistoryModal + history page: "INBOUND" → "입고", TRANSFER 시 "→ XX창고/← XX창고" 표시

3) 자동조회 (조회 버튼 없이 즉시):
   - m/orders (내발주이력): 날짜 from~to + 상태 input 추가 + state dep useEffect
   - m/orders/new (출고요청): "재고있는 품목만 / 전체 품목" 필터 추가 + 250ms 디바운스 자동 fetch

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:22:28 +09:00
chpark bf2339c242 feat(inbound+inventory): 입고는 입금완료(PAID) 만 + 재고이력 메뉴/모달
Deploy momo-erp / deploy (push) Successful in 2m17s
1) 입고 처리 (m/admin/inbounds):
   - 노출 대상: REQUESTED+PARTIAL → PAID+PARTIAL 로 변경 (입금 안 된 발주는 입고 불가)
   - editable 조건 / STATUS 라벨/색상 PAID 추가
   - 안내 문구 갱신

2) 재고이력 메뉴 (마이그레이션 025):
   - M_AINVH '재고이력' 메뉴 매입 그룹 sort 34 에 INSERT (페이지는 기존 /m/admin/inventory/history 활용)

3) 재고관리 페이지 (m/admin/inventory):
   - 데스크탑 표/모바일 카드 모두 행마다 "이력" 버튼 추가
   - 클릭 시 StockHistoryModal 팝업 — 해당 품목 + 해당 창고 한정 이력 조회 (POST /api/m/inventory/history)

매입 흐름 완성: 매입발주(REQUESTED) → 입금관리(PAID) → 입고처리(RECEIVED)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:03:29 +09:00
chpark b781722614 feat(proc-payment): 매입 입금관리 메뉴/페이지/API 신설
Deploy momo-erp / deploy (push) Successful in 2m9s
매입 흐름: 매입발주(REQUESTED) → 매입 입금관리(PAID) → 입고 처리(RECEIVED)

DB (마이그레이션 024):
- momo_procurements 에 paid_date, paid_amount, paid_method, paid_memo 컬럼 추가
- 매입 그룹 메뉴 sort 재정렬: 매입발주 30, 입금관리(신설) 31, 입고처리 32, 재고관리 33
- M_APROCPAY '매입 입금관리' /m/admin/proc-payments 메뉴 INSERT (ON CONFLICT idempotent)

UI/API:
- /m/admin/proc-payments 페이지 — 발주요청/입금완료 분리 카드 + 입금 처리 모달 (금액/방법/메모)
- 조회조건: 날짜 from~to + 공급업체 + 상태 (즉시 반영)
- POST /api/m/admin/proc-payments/list — REQUESTED|PAID 만 노출
- POST /api/m/admin/proc-payments/confirm — REQUESTED → PAID 전환 + paid_* 채움

다음 단계 (별도 batch): 입고 처리 페이지에서 PAID 만 노출 + 입고 시 RECEIVED 전환

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:30:31 +09:00
chpark 9293029631 chore: 제조사 관리 메뉴/페이지/API 삭제 + 엑셀 공급업체 80개 일괄 등록
Deploy momo-erp / deploy (push) Successful in 2m0s
1) 삭제:
   - src/app/(main)/m/admin/makers/page.tsx
   - src/app/api/m/makers/{list,save,delete}/route.ts
   (메뉴 DB 의 제조사 항목은 이전 commit 9705a04 에서 이미 제거됨)

2) 마이그레이션 023:
   - docs/모모유통 제조사 리스트(26.05.12).xlsx 의 80개 업체를 supply_mng 에 일괄 등록
   - idempotent: supply_name 중복 시 SKIP (NOT EXISTS)
   - supply_code: MM-NNNN 자동 채번 (기존 max(objid) + ROW_NUMBER)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:13:19 +09:00
chpark b8d0200831 fix(orders): 내발주이력 "동작" 컬럼 제거 — 행 클릭으로 상세 모달 열도록 통일
Deploy momo-erp / deploy (push) Successful in 2m4s
기존: 마지막 컬럼에 "보기" 버튼 + tr onClick 둘 다 있어 중복
변경: 동작 컬럼 + 보기 버튼 제거. tr 자체 클릭 시 상세 모달 (openDetail) 호출 — 이미 onClick 박혀있어 동작 동일.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:05:54 +09:00
chpark 7151a401d4 fix(orders): 일반 사용자 권한 매칭 (objid + userId) + 출고요청 리스트 가로스크롤 제거
Deploy momo-erp / deploy (push) Successful in 2m56s
권한 fix — order.customer_objid 와 매칭 시:
- 기존: user.objid 만 비교 → FITO 사용자(objid 없음)는 항상 fail → "권한 없음"
- 변경: user.objid ?? user.userId 와 customer_objid 매칭, 또는 user.userId 와 직접 매칭
- 적용: items/update, lines/save, cancel, statement, items/remark (5개 API)

출고요청 리스트(m/orders/new) UI:
- table 의 min-w-[640px] 제거 + table-fixed 적용
- 수량 컬럼 폭 180px → 112px, "담기" 버튼 텍스트 제거 (+ 아이콘만)
- 폰트 11~12px 로 축소
- 모바일 화면 한 줄에 들어오도록

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 11:49:26 +09:00
chpark a8049f57a6 feat(invoices): 계산서 발행 페이지 종합 개선 + deploy.yml 충돌 우회
Deploy momo-erp / deploy (push) Failing after 4m37s
invoices(계산서 발행) page:
- 조회조건 추가: 거래처(SearchableSelect) / 날짜 from~to / 상태
- 조회 버튼 제거 — 입력하면 즉시 클라이언트사이드 필터 적용
- "조회 결과 합계" 카드: 면세 / 과세(공급+세액) / 합계 분리 표시
- "선택 합산" 카드: 체크박스로 고른 건들의 면세/과세/합계 실시간 합산
- 표 행마다 면세/과세 컬럼 추가
- 전체 선택 체크박스 (헤더)

deploy.yml:
- docker compose up 흐름 강화: down --remove-orphans 후 docker rm -f momo-erp 로 잔존 컨테이너 강제 제거 + --force-recreate
- 수동 SSH 배포 + 자동 배포 겹쳤을 때 "container name already in use" 충돌 자동 해소

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 11:40:19 +09:00
chpark 80d2240a23 feat(items+einvoices): 품목 상태값 제거 + 공급업체/거래처 조회조건 + 합계 면세/과세 분리
Deploy momo-erp / deploy (push) Failing after 2m35s
- 품목관리(items):
  · STATUS 컬럼 표시/필터/폼에서 제거 (전부 ACTIVE 가 default — 사용자 사용 안 함)
  · 조회조건에 공급업체 SearchableSelect 추가 (이미 백엔드 vendorObjid 지원)
- 계산서 발행(einvoices):
  · 조회조건에 거래처 SearchableSelect 추가 (customers list API 사용)
  · 페이지 하단 tfoot 에 면세 합계 / 과세 합계 / 총 합계 분리 표시

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 11:29:55 +09:00
chpark 9cd9e5c0fd fix(orders): 거래명세표 캡처 레이아웃 다듬기
Deploy momo-erp / deploy (push) Successful in 2m47s
- 결제 계좌번호 셀: <br/> 줄바꿈 제거 + whitespace-nowrap + width 110px 로 한 줄 표시
- 총 합계 (VAT포함) 셀: whitespace-nowrap + 폰트 11px 로 좁은 셀에서도 한 줄
- 거래처 이메일(귀하 아래 라인) 표시 제거 — 외부 공유용 이미지에 사용자 이메일 노출 불필요

m/admin/orders 와 m/orders 두 곳 동일 적용.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 11:21:28 +09:00
chpark 083188332c feat(orders): 관리자가 출고완료(SHIPPED) 건도 수정 가능하도록 권한 확장
Deploy momo-erp / deploy (push) Successful in 5m18s
기존: REQUESTED 상태만 수정 가능 (admin/user 동일)
변경:
- USER: REQUESTED 만 (기존 그대로)
- ADMIN: PAID/CANCELED 가 아니면 모두 (REQUESTED / SHIPPED 등 입금완료 전까지)

items/update, lines/save 두 API 동일 적용. 입금완료(PAID) 이후나 취소건은 admin 도 수정 불가.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 11:08:02 +09:00
chpark 91ab88a359 fix(orders): 이미지 공유 버튼을 admin 만 노출하도록 보수적으로 변경
Deploy momo-erp / deploy (push) Successful in 2m47s
기존: user.role === "USER" 일 때 숨김 → 거래처라도 role 필드 비어있으면 노출되는 버그
변경: user.isAdmin === true || role === "ADMIN" 일 때만 노출 → 그 외는 모두 숨김
admin 계정만 거래명세표 이미지 공유 가능.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:49:08 +09:00
chpark bfb9470c85 fix(mobile): 뒤로가기 토스트가 페이지 이동 후에도 작동하도록
Deploy momo-erp / deploy (push) Successful in 2m59s
기존: mount 시 한 번만 history sentinel push → 사용자가 navigation 하면 sentinel 잃어버려 토스트 안 뜸.
변경: usePathname 의존성 useEffect → pathname 변경마다 sentinel 새로 push. lastBackRef 도 ref 로 변경(렌더 의존성 없이 상태 유지).
+ swal toast z-index 9999 강제 (다른 모달 위)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:45:14 +09:00
chpark 1e0a2640e9 feat(mobile): 로그아웃 모바일 분기 + 뒤로가기 토스트 활성화 조건 완화
Deploy momo-erp / deploy (push) Failing after 3s
- middleware: User-Agent 가 모바일이면 /login → /m/login 으로 redirect (서버 측 분기)
- auth-store.logout: window 가 모바일이면 /m/login, 아니면 /login (클라이언트 측 분기)
- BackButtonGuard: TWA 일부 환경에서 display-mode 가 standalone 으로 보고되지 않는 케이스 대응 — fullscreen/minimal-ui 도 포함, 모바일 UA 면 무조건 활성화

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:34:26 +09:00
chpark 0e9378a638 feat(orders): 사용자(USER)의 내출고이력에서 이미지 공유 버튼 숨김
Deploy momo-erp / deploy (push) Failing after 4s
m/orders 페이지의 거래명세표 DetailModal 에서 "이미지 공유" 버튼이 모든 사용자에게 노출되던 문제.
useAuthStore 의 user.role === "USER" 이면 버튼 hide. ADMIN 은 그대로 표시.

엑셀 다운로드 버튼은 그대로 유지 (사용자가 자기 주문 명세는 엑셀로 받을 수 있음).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:26:26 +09:00
chpark d8d508a78f feat(mobile): 로그인 스플래시 표시 시간 1.5초 → 3초로 늘림
Deploy momo-erp / deploy (push) Failing after 4m23s
사용자 요청으로 모모 로고 splash 가 더 충분히 보이도록 setTimeout 값 변경.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:04:25 +09:00
chpark 1039a11bbf feat(mobile): 안드로이드 뒤로가기 이중확인 + 모바일 로그인 스플래시
- src/components/back-button-guard.tsx 신규: standalone(PWA/TWA) 모드에서만 작동
  · 첫 뒤로가기 → sweetalert2 toast("한 번 더 누르면 앱이 종료됩니다") 표시
  · 2초 안에 두 번째 뒤로가기 → history.back() 으로 native back 위임 → 앱 종료
  · 일반 브라우저(non-standalone) 사용자에게는 영향 없음
- src/app/layout.tsx 의 RootLayout 에 BackButtonGuard 마운트
- src/app/(auth)/m/login/page.tsx 에 1.5초 스플래시 overlay 추가
  · 모모 로고 + "모모유통 ERP" + spinner ("로딩 중...")
  · z-60 fixed inset-0, 1.5s 후 opacity fade-out

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:47:35 +09:00
chpark 550fb12913 feat(capture): 거래명세표/매입발주서 이미지 공유 가로 출력
- src/lib/capture-share.ts 에 forceWidth 옵션 추가 → 캡처 직전 임시로 node width 강제 + 즉시 원복
- 출고 처리(거래명세표) 와 매입발주서 관리의 이미지 공유 호출에 forceWidth: 1100 적용
- 모바일 화면(좁은 viewport)에서 좁아진 표/품명 셀이 한 줄로 펼쳐져 엑셀 가로 출력처럼 캡처됨
- m/orders 페이지의 inline captureAndShare 를 capture-share lib 으로 통일 (toJpeg fallback / AbortError 처리 공유)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:44:37 +09:00
chpark 20e6255aa3 feat(auth): 모바일 전용 로그인 페이지 추가 (/m/login)
- src/app/(auth)/m/login/page.tsx 신규 — 한 화면에 딱 맞는 모바일 layout (logo + form + 푸터, safe-area inset 적용)
- middleware.ts publicPaths 에 /m/login + PWA 자원(/manifest.json, /sw.js, /.well-known) 추가
- 세션 있는 상태로 /m/login 진입 시 /m/dashboard 로 자동 redirect
- manifest.json 의 start_url 을 /m/login 으로 변경 → TWA APK 가 앱 실행 시 바로 로그인 화면

로그인 성공 시 /m/dashboard 로 이동 (기존 /login 은 API 응답의 redirectTo 사용, 모바일은 hardcode).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:44:37 +09:00
chpark 451117cfbe chore(pwa): assetlinks.json 에 APK 서명 SHA-256 fingerprint 박음
v1.0.0 APK (com.momotogether.erp) 의 서명 인증서 SHA-256 을 Digital Asset Links 에 등록.
이 commit 이 production 에 배포되면 TWA 앱이 URL bar 없이 풀스크린으로 동작.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:35:53 +09:00
chpark b343565bc1 feat(pwa): PWA + TWA 화 (Android APK 빌드 대비)
- public/manifest.json + service worker(sw.js) 추가
- icon PNG 변환 (192/512/180)
- public/.well-known/assetlinks.json placeholder (Bubblewrap 빌드 후 APK 서명 SHA256 채울 자리)
- layout.tsx 에 manifest/theme-color/apple-touch-icon 메타데이터 + 서비스 워커 등록 스크립트 추가

Bubblewrap 으로 APK 빌드 시 https://www.momotogether.com/manifest.json 을 source 로 사용.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:19:42 +09:00
chpark 7d7b22f388 docs: 인프라 이관 가이드 추가 (집 PC → IDC)
운영 서비스 목록, 데이터 볼륨 위치, 노출 포트, 운영 중 적용한 hotfix(Mailu DNSSEC/MariaDB 11.8 등), Phase 별 이관 체크리스트.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:17:57 +09:00
chpark 3785577442 chore: 원본 FITO Java/JSP 잔존물 제거 (Next.js 전환 완료)
WebContent/ (JSP + SmartEditor2 + 정적 자원) 와 src/com/ (Spring Controller/Service/Mapper) 디렉토리 전체 삭제. 총 2,191 파일.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:17:57 +09:00
chpark 4a00620d86 ci(deploy): 배포 후 dangling 이미지 자동 prune (옛 momo-erp sha 누적 방지)
Deploy momo-erp / deploy (push) Successful in 2m55s
docker compose build 시 latest 태그가 새 sha 로 갱신되면서 옛 sha 가 untagged
상태로 남아 매 배포마다 누적되던 문제. docker image prune -f 로 dangling 만 회수
(다른 프로젝트의 사용 중 이미지는 안 건드림).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 13:48:01 +09:00
chpark 1d3597ecb4 fix(dashboard): USER 분기 dead code 제거 (직전 commit 빌드 fail fix)
Deploy momo-erp / deploy (push) Successful in 23s
2026-05-12 11:56:37 +09:00
chpark 4206d57810 fix(dashboard): 거래처(USER) 진입 시 /m/orders/new 로 자동 이동
Deploy momo-erp / deploy (push) Failing after 3m42s
대시보드는 관리자 전용. USER 가 직접 URL 로 진입해도 즉시 출고 요청 화면으로
리다이렉트되게 차단. (메뉴 매핑 없어도 직접 URL 접근 방지)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:56:00 +09:00
chpark 9705a04328 feat(items): 제조사 필드/메뉴 제거 + 공급업체 검색 + 원가/단가 천단위 콤마
Deploy momo-erp / deploy (push) Failing after 1m31s
- 품목 폼/리스트/모바일 카드에서 제조사 컬럼·셀렉트 제거 (dead code 정리)
- 공급업체 셀렉트 → SearchableSelect (결과내 검색 가능)
- 단가/원가 인풋: type=number → text + 천단위 콤마 표시, 소수점 제거(반올림)
- 운영 menu_info: '제조사 관리' (9000204) status='inactive'

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 10:54:32 +09:00
chpark 52084d6075 feat(statistics): '업체별 발주통계' 명확화 + SHIPPED 필터 제거
Deploy momo-erp / deploy (push) Failing after 1m29s
- 화면 제목: '통계 — 업체별 월간 매출' → '업체별 발주통계 (월별)'
- 메뉴명: '월간 매출' → '업체별 발주통계' (운영 DB menu_info 9000501)
- statistics/monthly API: status IN 에서 SHIPPED (dead code) 제거
- 기존 기능 그대로: 년/월 선택, 업체별 합계 + 면세/과세 분리, 엑셀 다운로드

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 10:28:04 +09:00
chpark f62660952b fix(migration): 009 의 DELETE FROM user_info 비활성화 — 매 배포마다 거래처 삭제 사고
Deploy momo-erp / deploy (push) Successful in 1m54s
[원인]
- db/migrations/009_items_user_permissions.sql 가 user_type<>'C' AND
  NOT IN (admin 7인) 사용자를 삭제하는 정리 쿼리를 포함
- user_type 'C' → 'U' 통합 이후 'U' 거래처 134명이 위 조건에 걸려
  매 배포마다 통째로 삭제됨 (어제·오늘 두 번 사용자 관리에 거래처 0명)

[수정]
- 해당 DELETE 블록 통째로 주석 처리 — 마이그레이션은 idempotent 해야 하고
  destructive 작업은 두지 않는다는 원칙
- 거래처 134명은 별도 복구 스크립트로 다시 INSERT (이 commit 직후)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 10:20:02 +09:00
chpark f6e1436252 fix(items): 숨김 품목이 권한 없는 사용자에게 노출되던 문제
Deploy momo-erp / deploy (push) Successful in 1m44s
[원인]
- lib/auth.ts verifyCredentials 는 user.role 을 설정하지 않음
- /api/m/items/list 가 `r.user.role === 'USER'` 만 체크 → 일반 거래처도
  isUser=false 가 되어 status='ACTIVE' 필터 & view_hidden 필터 모두 우회
- 결과: 골드망고(status=active, is_hidden=Y) 가 모든 사용자에게 보임

[수정]
- isAdmin = role==='ADMIN' || isAdmin || userType==='A' (3가지 모두 검사)
- isUser = !isAdmin
- items/list: status 'ACTIVE' 비교를 UPPER(...) 로 대소문자 안전화
- orders/save: 숨김 품목(is_hidden='Y') 발주 시도를 view_hidden 권한 없으면 차단
- orders/new 클라이언트의 unlimitedQty 판정에도 userType==='A' 보강

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 10:14:36 +09:00
chpark 1e631c9181 fix(user-form): 부서 셀렉트 매칭 + 사번 제거 + 컴팩트 레이아웃
Deploy momo-erp / deploy (push) Failing after 1m33s
[부서 안 선택되던 문제]
- /api/admin/dept 가 DEPT_CODE/DEPT_NAME 대문자 반환인데 폼은 dept_code 소문자
  로 접근 → 옵션 매칭 실패. 대문자로 통일

[필드 제거]
- 사번(sabun) 입력 제거 (요청)
- dead var isCustomer 제거

[레이아웃 컴팩트화 — 스크롤 없이 한 화면]
- 폰트 13→12, 인풋 h-9→h-8, 여백/마진 축소
- 출고 기준 창고 + 특수 권한 섹션을 별도 큰 카드 → 2열 그리드 안에 통합
- 특수 권한 체크 라벨도 컴팩트 (가로형 inline 칩)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 09:58:51 +09:00
chpark 4e85dd56fe fix(user_type): 유형을 관리자(A)/사용자(U) 2가지로 통일
Deploy momo-erp / deploy (push) Successful in 1m42s
거래처/협력사 구분 제거 — 거래처 = 일반 사용자(U).
공급업체는 별도 supply_mng 테이블(공급업체 관리)에서 관리.

[UI]
- user-form 유형 셀렉트: 관리자/사용자 2개만 (거래처·협력사 제거)
- UserManagement 검색 구분 셀렉트: 관리자/사용자만
- profile 라벨: USER_TYPE='A' 면 '이름', 그 외 '업체명/이름'

[API/로직]
- signup: user_type='C' → 'U' / user_type_name '거래처' → '사용자'
- customers/list & customers/save: user_type='C' 강제 → user_type != 'A'

[운영 DB]
- 기존 user_type IN ('C','P') 134명 → 'U' 일괄 변경
  (분포: A 7명 / U 134명)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 00:53:33 +09:00
chpark a940c0735b fix(orders/new): ListView 에 unlimitedQty prop 전달 (빌드 fail fix)
Deploy momo-erp / deploy (push) Successful in 1m42s
직전 commit 9e9922e 에서 ListView 내부에서 부모 scope 의 unlimitedQty 를
직접 참조 → TS2304. props 로 명시 전달

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 00:48:44 +09:00
chpark 9e9922e219 feat(perm): 사용자 특수권한(발주한도 무시·숨김 품목 보기) UI 노출 + 출고요청 반영
Deploy momo-erp / deploy (push) Failing after 43s
[사용자 관리]
- /api/admin/users 목록에 UNLIMITED_QTY / VIEW_HIDDEN / USER_TYPE 컬럼 반환
- UserManagement 그리드에 '발주한도무시' / '숨김품목보기' 컬럼 추가 (/—)
- 사용자 수정 폼: '거래처 특수 권한' → '특수 권한 (발주 시 적용)' 으로 라벨 변경,
  거래처(C) 전용이던 조건을 풀어서 일반 사용자(U) 도 권한 부여 가능

[출고요청 (/m/orders/new)]
- /api/auth/me 가 unlimitedQty / viewHidden 반환
- 클라이언트가 unlimitedQty true 면 MAX_ORDER_QTY 무시하고 재고만큼 발주 가능
- '한도 ≤ N' 라벨도 권한자에겐 숨김

(백엔드 검증 — /api/m/items/list 의 view_hidden, /api/m/orders/save 의
 unlimited_qty 우회 — 는 이미 구현돼 있어 그대로 동작)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 00:46:37 +09:00
chpark 77d89527b8 fix(dept): 부서코드/회사명 입력 숨김 + 자동생성 + DB 정리
Deploy momo-erp / deploy (push) Failing after 1m30s
[UI]
- 부서 등록 폼: 부서코드/회사명 입력 제거 (부서명만 입력)
- 부서 목록 grid: 부서코드/회사명 컬럼 제거 (부서명/활성여부/등록일만)

[API]
- /api/admin/dept/save: dept_code 비어있으면 'DEPT' + MAX+1 자동 생성
- company_name 기본값 '모모유통'

[운영 데이터 정리]
- 거래처(user_type='C') 134명 dept_code → DEPT003 일반구매자
- 전체 사용자 141명 비밀번호 → '1' (AES 암호화 저장)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 00:39:15 +09:00
chpark 2ea2a6759e fix(auth): 권한그룹 코드 입력칸 숨김 + 자동생성
Deploy momo-erp / deploy (push) Failing after 1m31s
- 권한그룹 생성 모달에서 권한CODE 입력 제거 (권한명만 입력)
- 서버: 신규 등록 시 auth_code 비어있으면 GRP_<base36 timestamp> 자동 생성
- 좌측 권한 목록에서도 코드 노출 제거 (내부 식별자만 유지)
- 수정 시 기존 auth_code 는 보존 (COALESCE)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 00:30:55 +09:00
chpark 52db6eff53 fix(menu): isAdmin 백도어 제거 — 권한 그룹 매핑대로 동적 노출
Deploy momo-erp / deploy (push) Failing after 1m32s
기존 SQL 은 $isAdmin=true 면 모든 메뉴 통과 → 권한 매핑이 의미 없었음.
사용자 요청: "로그인한 사용자 권한 그룹에 따른 메뉴가 동적으로 나오도록"

- /api/menu: isAdmin 분기 제거, authority_sub_user JOIN authority_sub_menu
  매핑만 사용. 자식이 권한에 있으면 부모도 자동 노출(트리 유지) 로직은 유지
- 운영 DB: 관리자그룹에 active 메뉴 36개 일괄 매핑 (재고 이력 9000304 포함).
  사용자 관리 화면에서 권한 그룹 멤버 / 메뉴 매핑을 직접 조정해 사용자별
  사이드바를 동적으로 제어

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 00:26:22 +09:00
chpark 10e8dad841 fix(user/default_wh_objid): numeric → text 로 변환
Deploy momo-erp / deploy (push) Successful in 1m42s
운영 momo_warehouses.objid 가 text 타입(예: MOMOWH000000001)이라
default_wh_objid 도 text 로 일치시켜야 매핑 가능.
- db/migrations/022_user_default_wh_text.sql
- /api/admin/users/save: ::numeric 캐스트 제거

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:36:49 +09:00
chpark 3ba6237a32 feat(orders+users): 사용자별 기준 창고 매핑 → 출고 시 그 창고에서 차감
Deploy momo-erp / deploy (push) Successful in 1m49s
본사팀 / 김포팀 거래처 리스트처럼 거래처별로 출고 창고가 다른 케이스 대응.
회원은 가입 시 기본 정보만 입력하고, 관리자가 사용자 수정에서 창고를 매핑.

[스키마]
- db/migrations/021_user_default_wh.sql: user_info.default_wh_objid numeric NULL

[API]
- /api/admin/users/detail: default_wh_objid 반환
- /api/admin/users/save: 수정 시 default_wh_objid 저장 (빈 값 → NULL)
- /api/m/orders/approve: 1) 거래처의 default_wh_objid → 2) STOCK 첫 창고 fallback

[UI]
- admin-panel/user-form: '출고 기준 창고' 셀렉트 추가 (수정 모드만)
  미지정 시 기본 창고로 fallback

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:18:22 +09:00
chpark ed66ca36eb fix(orders+payments): SHIPPED 상태 중복 라벨 제거 + 입금관리 검색조건 추가
Deploy momo-erp / deploy (push) Successful in 48s
[출고 상태 셀렉트 중복 fix]
- STATUS_LABEL 에 APPROVED='출고완료' / SHIPPED='출고완료' 둘 다 매핑돼
  셀렉트 옵션에 '출고완료'가 두 번 노출됐음. 운영 DB 분포 확인 결과
  SHIPPED 상태값은 0건(dead) → 라벨/색상 매핑에서 SHIPPED 제거.
  StatementPreview 의 'SHIPPED' OR 분기도 정리

[입금 관리 검색조건]
- 시작일 / 종료일 / 입금 상태(전체·입금 전·입금완료) / 업체명·발주번호 키워드
- 기본 기간: 이번달 1일 ~ 오늘
- 입금 상태: UNPAID = APPROVED, PAID = PAID + INVOICED 묶어서 필터
- 초기화 / 조회 버튼

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:09:22 +09:00
chpark d5954d39e9 fix(procurements): 발주서 품명 옆 [품목코드] 노출 제거
Deploy momo-erp / deploy (push) Failing after 35s
거래처에 보내는 양식이라 내부 ITEM_CODE 는 보일 필요 없음.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:05:42 +09:00
chpark 8a263467ba fix(inventory/history): 모바일 카드 + 검색바 반응형
Deploy momo-erp / deploy (push) Successful in 47s
- 기존: <table overflow-hidden> 으로 모바일에서 가로 컬럼이 잘려 스크롤이 안 됨
- 데스크톱(sm 이상): hidden sm:block + overflow-x-auto + min-w-[760px]
- 모바일(sm 미만): sm:hidden 카드 — 유형 배지/품목명/수량/창고/메모/처리자/일시
  → 페이지 자체 스크롤로 자연스러운 UX
- 검색바 모바일 1열 → sm 3열 → lg 5열, 요약카드 폰트도 화면별 분기

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:04:20 +09:00
chpark d1683a3c6e feat(inventory+warehouse): 재고 이동 + 모바일 카드 + 창고코드 자동생성
Deploy momo-erp / deploy (push) Failing after 35s
[재고 관리]
- 새 기능 '재고 이동' (A창고 → B창고): 출발창고 잠금 + 충분재고 검증 + 도착
  창고 upsert + 이동 로그 2건(OUT/IN, ref_type=TRANSFER) 트랜잭션 처리
- /api/m/inventory/transfer 신규
- 모바일에서 테이블 가로 스크롤이 안 되던 문제 → sm:hidden 카드 + sm:block
  desktop 테이블로 분리. 페이지 자체 스크롤로 자연스러운 UX
- 검색 영역 모바일 1열 / sm 3열 그리드 정리
- 재고 이동 모달은 출발창고 선택 시 그 창고에 재고 있는 품목만 셀렉트
- list API 응답에 WH_OBJID 추가 (이동 모달에서 출발창고 필터 용도)

[창고 관리]
- 창고 코드는 자동생성(WH001, WH002 ...) — 등록/수정 폼에서 readonly + 회색.
  save API: regist 시 nextWhCode() 로 MAX+1 패딩. update 시 wh_code 미변경
- 클라이언트가 whCode 보내도 무시되도록 서버에서 분기

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 16:34:19 +09:00
chpark 2d40678358 feat(dashboard+orders): 카드별 정확한 필터 전달 + 출고요청 카드 컴팩트화
Deploy momo-erp / deploy (push) Failing after 35s
[대시보드 → 출고처리 카드 필터]
- 승인 대기 / 진행중 / 미수금 → ?status=...&dateFrom=&dateTo= (전체 기간, 빈 날짜)
- 오늘 발주 → ?dateFrom=오늘&dateTo=오늘
- 이번달 매출/누적 → ?dateFrom=이번달1일&dateTo=오늘
- orders 페이지: 쿼리에 dateFrom/dateTo 키가 있으면(빈값 포함) 그 값 사용,
  키가 아예 없을 때만 기본값 오늘. 사용자 모드 페이지도 동일

[출고 요청 카드 그리드]
- grid-cols-3 / md-4 / lg-5 — PC 5개·모바일 3개/줄
- 카드 padding p-3~p-4 → p-2, 폰트/버튼/이미지 라벨 모두 컴팩트
- IS_TAX_FREE/REQUIRES_DELIVERY 배지를 이미지 위 좌상단으로 이동해 공간 절약
- 품목명 line-clamp-2 + min-h-[2em] 로 카드 높이 일정화

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:45:49 +09:00
chpark ebabd726f3 feat(admin/orders): 검색 조건 + 모바일 레이아웃 + 페이지명 단순화
Deploy momo-erp / deploy (push) Successful in 50s
- 페이지명: '발주서 관리 · 출고처리' → '출고 처리'
- 검색바 신규: 시작일 / 종료일 / 상태 / 검색어(발주번호·업체명·이메일) / 초기화·조회
- 모바일 1열, sm 2열, lg 5열 그리드로 반응형 정리
- API list: keyword 파라미터 추가 (order_no/user_name/email LIKE)
- 기본 기간 = 오늘. 단 ?status= 으로 진입(대시보드 카드)한 경우 30일 범위
- 검색 조건 변경은 [조회] 버튼으로만 트리거 (자동 reload 제거 → 입력 도중 깜빡임 X)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:00:47 +09:00
chpark 5b457b8f0d feat(menu): 사이드 메뉴 API 에 권한그룹 필터 적용
Deploy momo-erp / deploy (push) Failing after 36s
- /api/menu: super admin 은 모든 메뉴, 일반 사용자는 authority_sub_menu 매핑된
  메뉴만 노출. 자식이 권한에 있으면 부모 메뉴도 자동 포함 (트리 유지)
- 권한 관리 화면에서 메뉴 체크 → 다음 로그인부터 사이드바 즉시 반영

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 14:54:15 +09:00
chpark a3ab0d7629 feat(admin/auth): 권한관리 통합 화면 — 권한그룹/직원/메뉴 동시 매핑
Deploy momo-erp / deploy (push) Failing after 37s
[권한그룹 사용자 추가 SQL 에러 fix]
- $3 파라미터에 ::text 명시 캐스트로 inconsistent types 해결
  ("inconsistent types deduced for parameter $3")

[새 UI - admin-panel/auth]
- 좌측: 권한 목록 + 검색 + 생성 (목록에서 클릭으로 활성화, 더블클릭으로 수정/삭제)
- 우중·우우: 권한있는/권한없는 직원 패널 (체크박스 + 전체선택 + 검색)
  · ‹ 추가  / 제거 › 버튼 즉시 반영
- 하단: 메뉴 전체 트리 (체크 즉시 서버 반영)
- 모달 헬퍼 안 띄우고 한 화면에서 모두 처리 → 사용 흐름 단순화

[새 스키마/API]
- db/migrations/020_authority_sub_menu.sql
- /api/admin/auth/menus  : 그룹의 메뉴 OBJID + 전체 메뉴 트리
- /api/admin/auth/menus/toggle : 단일 메뉴 ON/OFF

[거래명세표]
- 수량 컬럼 너비 w-14 → w-20 (모바일에서 잘리던 문제)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 14:36:16 +09:00
chpark 004a8e4a6b feat(dashboard): 카드 클릭 → 해당 리스트로 즉시 이동
Deploy momo-erp / deploy (push) Failing after 41s
- 관리자: 승인 대기 → /m/admin/orders?status=REQUESTED, 오늘/이번달 → /m/admin/orders,
  미수금 → /m/admin/payments
- 사용자: 대기중 → /m/orders?status=REQUESTED, 진행중 → ?status=APPROVED, 미수금/누적
  → /m/orders
- orders 페이지가 ?status= 쿼리로 초기 필터 자동 적용

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 14:15:38 +09:00
chpark 23599b9c18 fix(capture): Windows Chrome 에서 이미지 캡처 실패 — 외부 폰트 임베드 회피
Deploy momo-erp / deploy (push) Failing after 42s
- html-to-image 의 toPng 가 Pretendard CDN 임베드 단계에서 fail 하면 캡처 전체가
  깨짐 (Windows Chrome 에서 자주 발생). skipFonts + cacheBust + jpeg fallback 추가
- 거래명세표(orders) / 발주서(procurements) 양쪽이 같은 코드를 복붙으로 갖고 있던
  걸 lib/capture-share.ts 로 통합
- 실패 시 err.message 를 swal 에 노출 (이전엔 "잠시 후 다시 시도하세요" 만 떠서
  사용자가 원인 추적 불가)
- navigator.share 의 AbortError(사용자 취소) 는 silent 처리 + 그 외엔 다운로드 폴백

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 14:10:52 +09:00
chpark ffada52fd4 ci: deploy 단계를 build → down → up 패턴으로 (컨테이너 이름 충돌 방지)
Deploy momo-erp / deploy (push) Successful in 52s
5cbc324 배포 시 --force-recreate 가 이름 충돌(Conflict, 65adeb31db46_momo-erp)을
일으켜 컨테이너 swap 실패. 명시적으로 down --remove-orphans 후 up 으로 분리.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 13:45:48 +09:00
chpark 5cbc324627 fix(admin): 권한 멤버 추가 fail 표시 + 사이드바 중복 제거 + 로그인 ID/PW 저장
Deploy momo-erp / deploy (push) Successful in 51s
- 권한그룹 멤버 추가/제거 API: history insert 를 best-effort 로 분리해 메인 INSERT
  실패가 누적 에러로 noisy 응답에 담김. 클라이언트는 fail 분기에서 swal 로 사유 표시
- admin-panel 좌측 사이드바: '메뉴관리' 카테고리는 항상 고정 노출되므로 DB groups
  에서 같은 라벨이 다시 내려와도 중복 렌더링 안 함
- 로그인 화면: '아이디/비밀번호 저장' 체크박스 추가 (localStorage SAVE_KEY).
  체크 후 로그인 → 다음 방문 시 자동 채움. 해제하면 즉시 삭제

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 13:44:25 +09:00
chpark faf8315260 fix(admin-panel): useSearchParams 제거 + deploy 강건성 개선
Deploy momo-erp / deploy (push) Failing after 43s
- useSearchParams 가 Next.js 15 prerender 단계에서 Suspense 경계를 강제해
  /admin-panel 빌드 자체가 실패 → docker image 재빌드 안 됨 →
  컨테이너 swap 누락(2시간째 옛 이미지). window.location.search 직접 읽기로 대체
- deploy.yml: set +e 제거 (빌드 실패가 워크플로우 success 로 묻히는 문제 차단)
- docker compose 에 --force-recreate 추가 (이미지가 같아도 컨테이너 강제 재생성)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 13:33:40 +09:00
chpark 93f27d70f2 ci: 헬스체크를 build-sha.txt 일치 검증으로 강화
Deploy momo-erp / deploy (push) Failing after 14m15s
- 기존 마커(WORKFLOW/매입 발주/SCREEN PREVIEW)는 옛 빌드에도 들어있어
  배포가 실패해도 success로 묻혔음 (사용자: 사이드바 변경이 운영에 안 반영됨)
- SSH 단계에서 git rev-parse HEAD → public/build-sha.txt 에 기록 후 빌드
- 헬스체크가 운영의 /build-sha.txt 를 GITHUB_SHA 와 비교 → 불일치면 워크플로우 fail

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:31:05 +09:00
chpark 63cdc6cab9 feat(menu): 사용자/관리자 모드 사이드바 분리 + 거래명세표 즉시출고
Deploy momo-erp / deploy (push) Successful in 36s
- 사이드바: '거래처' 키워드 필터 제거. 사용자 모드 = DB 권한 메뉴 전체,
  관리자 모드 = 시스템 관리 가상 카테고리(사용자/권한/메뉴/공통코드/로그)
- admin-panel: ?tab= 쿼리로 진입 탭 결정. 좌상단 '← 사용자' 복귀 링크
- header: admin 자동 admin 모드 진입 제거 (기본 사용자 모드)
- 출고관리 거래명세표 미리보기: 엑셀 다운로드를 이미지 공유/인쇄 옆으로
  이동, 출고요청 상태일 때 [출고] 버튼 추가하여 체크 없이 바로 처리
- 발주서 미리보기: [인쇄] 버튼 추가

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:18:59 +09:00
chpark 6cfe0041a2 feat: 매입 발주서 납품조건 + 공급업체 샘플 + 관리자/사용자 모드 토글 + 모달 sticky
Deploy momo-erp / deploy (push) Successful in 53s
[DB 019]
- momo_procurements 에 delivery_place / delivery_period / payment_terms / freight_terms 컬럼 추가
- 기존 supply_mng (공급업체) 데이터 모두 삭제 + 샘플 10개 신규 등록
  · (주)아바텍, 대성식품, (주)고기파는농부, 광이진천 농장, 단과일,
    봉담수산, 명일동유기농, 울산단과일, 농부의아침, 초록마을 도매
- 시퀀스 가정 없이 MAX(objid)+1 로 안전하게 부여

[발주서 양식 — 표준 거래명세표 양식 반영]
- ProcurementForm: "2. 납품조건" 섹션 추가
  · 1)~3) 표준 조항 (납기 지연 공제 / 검수 부적합 반출 / 수량 규격 변경)
  · 4) 납품장소 5) 납품기간 6) 대금지불 7) 운임부담 — 표 형식 입력칸
  · 8)~9) 표준 조항 (3일 이의 제기 효력 / 명시되지 않은 사항)
  · 하단 "상기와 같이 발주함." + 발주일 + 발주자
- update-header API: 4개 필드 동적 업데이트
- /api/m/procurements/excel/[id]: 엑셀 출력에도 납품조건 9개 항목 + 4필드 표
- /api/m/procurements/send: 메일 본문 HTML 에도 납품조건 표 + 표준 조항

[관리자/사용자 모드 토글]
- 헤더 매뉴얼 옆에 [👥 사용자 / 🛡 관리자] 토글 버튼 (admin 권한자만 노출)
- menu-store: viewMode("user"|"admin") + setViewMode 추가
- 사이드바: viewMode 에 따라 대메뉴 필터링
  · 사용자 모드: '거래처 주문' 그룹만
  · 관리자 모드: 출고/정산 + 매입/입고 + 마스터 관리 + 통계
- admin 권한자 자동으로 로그인 시 관리자 모드 진입

[ItemPicker 모달 모바일 친화]
- 모바일에서 화면 하단 도킹(items-end) → 풀스크린 시트 처럼
- 헤더는 sticky top-0 으로 고정 → 긴 목록에서도 검색바 항상 보임

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:42:45 +09:00
chpark 91a7eecc61 feat: 로그인 계속 유지 + 거래명세표 양식 정교화 + 미수액·비고
Deploy momo-erp / deploy (push) Successful in 51s
[로그인 — 계속 유지]
- session.ts: REMEMBER 30일 → 10년 (사실상 영구)
- 로그인 폼 라벨: "로그인 유지 (30일)" → "로그인 계속 유지"
  · 한 번 로그인하면 계속 자동 로그인 (브라우저/쿠키 유지되는 한)

[거래명세표 양식 정교화 — 엑셀 + 메일 본문]
- 첨부 엑셀(buildStatementXlsx) + 메일 HTML(buildStatementHtml) 모두 양식 통일
- 사용자 이미지의 표준 거래명세표 양식 반영:
  · 좌상단: 일자 + 거래처(귀하) 박스
  · 우상단: 공급자 표 (결제계좌·전화·이메일 — 회색 헤더 + 세로 "공급자" 라벨)
  · "아래와 같이 계산합니다." 가운데 정렬
  · 합계 (VAT포함) + 미수액 강조 박스 (노란색)
  · 표: 순번/품명/구분/단위/수량/단가/공급가액/세액/합계(노란 강조)/비고
  · 거래처용이라 현재고는 처음부터 컬럼에 없음
  · 비고 컬럼에 라인별 remark 표시 (statement API 응답에 remark 추가)
  · 미수액 = total - paid_amount (입금 등록되면 자동 차감 반영)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:30:42 +09:00
chpark 3a0400a0c2 feat: 거래명세표 캡쳐 시 현재고 숨김 + 로그인 유지(30일) 체크박스
Deploy momo-erp / deploy (push) Successful in 53s
[현재고 — 캡처/공유 시에만 숨김]
- 거래처에 보낼 이미지에서 내부 정보(현재고)가 보이면 안 됨
- 거래명세표 표의 현재고 th/td 와 재고 부족 경고 박스에 .js-no-export 클래스 추가
- captureAndShare 안에서 toPng 직전 임시로 display:none → 캡처 후 복원
- 화면에서는 그대로 보이고, 다운받은 PNG/공유 이미지에서만 빠짐

[로그인 유지 — 30일 세션]
- /api/auth/login 요청 body 에 remember 추가
- /lib/session.ts createSession(user, remember=false) — 24시간(기본) / 30일(remember=true)
- 로그인 폼에 [✓ 로그인 유지 (30일)] 체크박스 (기본 ON, 나이 많은 사용자 친화)
- 체크 해제하면 24시간 세션 유지 (기존 동작)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:13:28 +09:00
chpark 3cbb28bbbd fix(반응형): 출고관리·매입발주·입고처리 모바일 스크롤 가로채기 해제
Deploy momo-erp / deploy (push) Successful in 53s
증상: 모바일에서 좌-우 분할 화면(출고관리 등) 의 페이지 스크롤이 안 됨.
원인:
- 컨테이너 minHeight: calc(100vh - 200px) 가 모바일에서도 적용 →
  좌·우 박스가 각각 거의 풀높이 차지
- 박스 내부의 overflow-auto 가 페이지 스크롤을 가로채 박스 안만 스크롤됨

수정:
- minHeight 인라인 스타일 → lg:min-h-[calc(100vh-200px)] 로 lg 이상에서만 적용
- 좌측 리스트 컨테이너: max-h-[60vh] lg:max-h-none 로 모바일에서 자연스러운 높이
- flex-1 overflow-auto → flex-1 lg:overflow-auto (모바일은 페이지 스크롤로 통일)
- 동일 패턴을 매입발주(/m/admin/procurements) + 입고처리(/m/admin/inbounds) 에도 적용

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:02:12 +09:00
chpark a5fd64da62 feat(출고요청): 카드 안에서 수량 입력/조절 + 카드/리스트 토글 + 거래명세표 라인 sync 버그 수정
Deploy momo-erp / deploy (push) Successful in 53s
[출고요청 화면 개선 — /m/orders/new]
- 카드에 [수량 입력 + 담기] 한 번에. 엔터 또는 버튼 클릭 시 그 수량만큼 카트에 추가
- 이미 담은 품목은 카드 안에 [- 1 +] 컨트롤 + [×] 빼기 버튼이 즉시 노출
  · 카트 수량 그 자리에서 직접 수정. 카드 외 카트 펼치기 불필요
- 담은 품목 카드는 emerald 테두리 + 우상단에 "담은 N" 배지로 강조

[보기 모드 토글]
- 검색바 우측에 [카드 / 리스트] 토글
- 카드: 기존 그리드 (이미지 위주, 시각적)
- 리스트: 표 형태 (품목 많을 때 한눈에) — 행마다 동일 [수량+담기] 컨트롤

[관리자 거래명세표 라인 sync 버그 fix]
- /m/admin/orders 에서 [+택배/+용차] 클릭 시 합계만 올라가고 인풋 표시값이 안 바뀌던 문제
- ExtraRow key 를 `OBJID-QTY-UNIT_PRICE-LABEL` 로 변경해 line 변경 시 컴포넌트 강제 재마운트
- useState 초기값이 새 line 값으로 확실히 반영됨

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:02:14 +09:00
chpark 2f4c5d5d02 feat(반응형): 입금/계산서/공급업체/창고/제조사/품목 — PC + 모바일 통일 UI
Deploy momo-erp / deploy (push) Successful in 51s
[패턴]
- 데스크탑(sm 이상): 표 + 가로 스크롤 (min-w 명시)
- 모바일(<640px): 카드 리스트 — 한 행을 그대로 카드 한 장으로
- 헤더: title + 부제 + 추가 버튼이 flex-wrap 으로 재배치
- 모달: max-w 유지 + 모바일에서 max-h-[90vh] overflow-y-auto

[페이지별 변경]
- /m/admin/payments: 요약 카드 3장 (전체/처리됨/미수금) + 표/카드 분기
- /m/admin/invoices: 선택건수+합계 부제 + 카드 리스트 + 표
- /m/admin/vendors: 카드 클릭 → 수정 모달 직행, 데스크탑 표 분리
- /m/admin/warehouses: 카드(아이콘+유형 배지)+표, 유형 색상 매핑
- /m/admin/makers: 검색바 폭 자동 + 카드(아이콘+버튼)+표
- /m/admin/items: 카드(이미지 썸네일+모든 배지) + 표, min-w 900px

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:34:19 +09:00
chpark 1502960151 feat(모바일 반응형): 사이드바 햄버거 오버레이 + 자동 닫기
Deploy momo-erp / deploy (push) Successful in 51s
증상: 모바일로 로그인 시 사이드바가 콘텐츠를 덮어 사용 불가능.
원인: 사이드바가 모든 폭에서 항상 정상 폭으로 자리잡음.

[레이아웃]
- 사이드바를 모바일에서 fixed + translate-x-full 로 화면 밖에 두고,
  mobileOpen=true 시 translate-x-0 슬라이드 인 (200ms transition)
- 모바일 오버레이 배경 클릭 시 닫기
- lg 이상에서는 기존대로 좌측 고정

[헤더]
- 모바일에서만 햄버거(≡) 버튼 노출 → setMobileOpen(true)
- 사용자명 모바일 width 줄이고 부서명 숨김 (110px → sm 이상 200px)

[사이드바]
- 헤더 우측에 모바일 전용 X 버튼 추가 (lg:hidden)
- 데스크탑 햄버거 토글은 hidden lg:flex 로 분리
- handleSubMenuClick 에서 setMobileOpen(false) 호출 → 메뉴 선택 시 자동 닫힘

[스토어]
- mobileOpen 상태 + setMobileOpen 액션 추가

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:23:27 +09:00
chpark f85b2f17e0 feat(header): 상단에 [📖 매뉴얼] 링크 추가
Deploy momo-erp / deploy (push) Successful in 56s
요구사항: 메인 화면 어디서든 사용 설명서로 빠르게 이동.
- 헤더 우측 (사용자명 옆) 에 BookOpen 아이콘 + "매뉴얼" 텍스트
- /manual.html 새 탭으로 열기 (target=_blank)
- 모바일에서는 텍스트 숨기고 아이콘만

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:09:02 +09:00
chpark 88e7eab65e feat(입고 처리): 매입발주 선택 → 라인별 창고/수량 입고 (부분/전체) + 매뉴얼 보강
Deploy momo-erp / deploy (push) Successful in 51s
[입고 처리 화면 재설계 — 등록 → 수정 방식]
- 좌-우 분할:
  · 좌: 매입 발주서 리스트 (발주요청+입고중 기본 필터)
  · 우: 발주 라인별 [창고 선택 + 정상 입고 + 불량] 인라인 입력
- 발주/입고/미입고 한눈에 표시 (예: 10 / 5 / 5)
- 완전 입고된 라인은 ✓ 완료 표시 + 입력 칸 잠김
- 정상+불량은 남은 수량(qty - received_qty) 이하로 자동 클램프

[/api/m/procurements/list]
- 응답에 TOTAL_QTY, RECEIVED_QTY 추가 → 좌측 리스트에 진척 표시

[/api/m/inbounds/save]
- procObjid 있으면 라인별 입고 한도 사전 검증 (qty - received_qty 초과 차단)
- 0 입고 라인은 건너뛰기
- 매입발주 상태 자동 갱신:
  · 모든 라인 완전 입고 → RECEIVED (입고완료)
  · 일부 라인만 입고 → PARTIAL (입고중)
  · 시작 안 함 → REQUESTED 유지

[매뉴얼 — 가-1, 가-2, 다-2 대폭 보강]
- 거래처 출고 요청: 6단계 체크리스트 + 화면 도식 + 토스트/모달 예시 + 시나리오
- 내 주문 내역 + 거래처 자기 주문 수량 수정/품목 삭제/취소: 화면 도식 + 단계별 가이드 + 상태표
- 입고 처리: 화면 도식 + 발주/입고/미입고 표시 의미 + 부분입고 시나리오

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 01:16:29 +09:00
chpark da9b16f012 fix(품목 추가 모달): 체크박스 선택 안되던 문제 + 손모양 커서
Deploy momo-erp / deploy (push) Successful in 52s
증상: 품목 추가 모달에서 행 클릭과 체크박스 클릭 둘 다 toggle 호출 →
       두 번 발생해서 체크 상태가 변하지 않음.
수정:
- 체크박스 td 에 stopPropagation 추가 (행 onClick 으로 버블되지 않게)
- 행 클릭은 그대로 행 전체 토글로 동작
- 체크박스 명시 cursor:pointer + 크기 18px + 색상 accent-emerald-600
- select-none 추가 (드래그 시 글자 선택 방지)
- 헤더 전체선택 체크박스도 동일 처리

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 01:07:34 +09:00
chpark 03838af90c feat(거래처 출고이력): 본인이 출고요청 상태 품목 수량/삭제 직접 수정
Deploy momo-erp / deploy (push) Successful in 51s
[새 API: /api/m/orders/items/update]
- 본인 또는 관리자가 자기 발주의 품목(ITEM) 라인 수량 변경 또는 삭제
- REQUESTED 상태에서만 허용. 단가는 변경 불가 (momo_items.unit_price 기준 자동 재계산)
- 재고 / max_order_qty 한도 자동 검증 (unlimited_qty 권한이면 한도 우회)
- 트랜잭션으로 라인 수정 + momo_orders 합계 7종 자동 재집계

[/m/orders 거래명세표 모달 UI]
- 출고요청 상태 거래처 본인 화면에서 품목 라인 직접 편집:
  · 수량 인풋 (블러 시 자동 저장)
  · 행 끝의 [×] 버튼으로 그 품목만 삭제
- 택배/용차 라인은 인풋 안 보이고 "자동" 표시 — 모모 담당자가 조정
- 저장/삭제 후 onReload 로 모달 + 리스트 동시 갱신
- 안내 배너: "수량 수정 / 품목 삭제 / 주문 취소" 모두 가능 명시

[매뉴얼]
- 가-2 내 발주 이력 섹션에 수량 수정 / 품목 삭제 / 주문 취소 사용법 추가

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 00:38:23 +09:00
chpark e86017c42a feat(통계+세금계산서): 거래처×일자 피벗 통계 신설 + 중복 발행 차단
Deploy momo-erp / deploy (push) Successful in 53s
[새 통계 — 거래처×일자 매출 피벗]
- API: POST /api/m/statistics/monthly-pivot
  · 입력: { year, month }
  · 응답: dates[] / rows[ {거래처, BY_DAY:{날짜:{면세,과세}}, TOTAL_TAXFREE/TAXABLE} ] / totalsByDay / grandTotal
  · 출고완료/입금완료/계산서발행 상태 발주만 집계
- 화면: /m/admin/statistics/pivot
  · 가로 스크롤 피벗 표 (왼쪽 sticky 업체명)
  · TOT 행: 월간 일자별 총합 (부가세 신고용)
  · 거래처별 정렬: 매출 큰 순
  · 합계 카드 3종: 면세/과세/총
  · 엑셀 다운로드 (거래처 행 × 일자 컬럼 평면화)
- 메뉴 등록: 018 마이그레이션 (objid 9000504, 통계 그룹)

[세금계산서 중복 발행 차단]
- /api/m/einvoices/issue: orderObjid 가 이미 발행됨(FAIL/CANCELED 제외) 이면 400
  · "이미 발행된 발주입니다 (상태/승인번호)" 메시지 + alreadyIssued=true 플래그
- /m/admin/einvoices: 발행 가능 발주 리스트에서 이미 발행된 건 자동 제외
  · orders/list 와 einvoices/list 동시 조회 후 클라이언트 측 필터
  · DRAFT/QUEUED/SENT/ACK 모두 발행 완료로 간주 — 재발행 불가
  · FAIL/CANCELED 만 다시 발행 가능

[매뉴얼]
- 통계 표에 "거래처×일자 매출 (피벗)" 항목 추가, 부가세 신고 자료 활용 안내

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 23:51:05 +09:00
chpark b95ac11015 fix(items/list): 마이그레이션 미적용 환경에서도 동작하도록 컬럼 자동 보장
Deploy momo-erp / deploy (push) Successful in 51s
증상: 마스터 품목 관리 화면에 품목이 하나도 안 보임.
원인: 마이그레이션 016 (vendor_objid) 이 운영 DB 에 적용 안 된 상태에서
       SELECT I.vendor_objid 가 'column does not exist' 로 실패 → 빈 배열 응답.

해결: API 첫 호출 시 ALTER TABLE ... ADD COLUMN IF NOT EXISTS 로
       vendor_objid + max_order_qty + is_hidden + requires_delivery 모두 자동 보장.
       idempotent 하므로 이미 적용된 환경에서도 NOOP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 23:24:58 +09:00
chpark 58645a0b3f feat(매입 발주): 엑셀 다운로드 + 이미지 공유 + 품목조회 JOIN 캐스팅 수정
Deploy momo-erp / deploy (push) Successful in 53s
[발주서 엑셀 다운로드]
- /api/m/procurements/excel/[id] 신설
- 이미지의 표준 발주서 양식대로 .xlsx 생성
  · 분류번호/발주서번호/발주일/공급업체/연락처/이메일
  · 1.물품의 표시 (품목코드·품명·단위·수량·단가·금액)
  · 총액 + V.A.T 별도
  · 2.비고 + 발주자 정보

[발주서 이미지 공유]
- 매입 발주서 양식 우상단에 [📤 이미지 공유] [⬇ 엑셀 다운로드] 버튼
- html-to-image 로 PNG 캡처 → Web Share API (카톡 등) 또는 PNG 다운로드
- 거래명세표(출고/정산)와 동일한 사용자 경험

[버그 수정 — 품목 모달에 결과 안 나옴]
- /api/m/items/list 의 supply_mng JOIN 캐스팅 누락
  · momo_items.vendor_objid (TEXT) vs supply_mng.objid (NUMERIC) 타입 충돌로 SQL 에러 → 빈 배열 응답
- LEFT JOIN supply_mng V ON I.vendor_objid = V.objid::text 로 명시적 캐스팅

[매뉴얼]
- 매입 발주 섹션에 "발주서 공유 / 엑셀 다운로드" 안내 추가

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 23:23:21 +09:00
chpark 6b60178b1d feat(v0.7 round2): 매입 발주서 양식 + 좌-우 분할 + 공급업체 일괄 불러오기
Deploy momo-erp / deploy (push) Successful in 51s
[화면 — /m/admin/procurements 전면 개편]
- 좌측: 발주서 리스트 (상태 필터, 발주번호, 공급업체, 금액)
- 우측: 발주서 양식 (이미지의 표준 발주서 형태)
  · 분류번호/발주서번호/발주일/공급업체 표
  · "1. 물품의 표시" 표 (품명·단위·수량·단가·금액)
  · "2. 비고" 텍스트 영역
  · 합계 자동 계산
- [+ 새 발주] / [발주 요청] 상단 버튼
- 작성중(OPEN) 상태에서만 인라인 편집 가능, 발주요청 후 잠김

[품목 추가 모달]
- 검색 + [공급업체 필터(현재/전체)] + [결과 내 검색]
- 다중 선택 + 헤더 체크박스로 전체 선택
- 이미 담긴 품목은 '이미' 표시
- 한 번에 N개 일괄 추가 (수량 1, 원가는 품목 마스터의 cost_price)

[API 4종 신설]
- POST /api/m/procurements/create-empty: 빈 발주서 1건 생성 (proc_no 자동 부여, status=OPEN)
- POST /api/m/procurements/lines/save: 라인 추가/수정/삭제 + 합계 재집계 (트랜잭션)
  · 같은 품목 중복 추가 시 수량 누적
- POST /api/m/procurements/update-header: 공급업체/메모 수정
- POST /api/m/procurements/send: 발주 요청 — status OPEN→REQUESTED + 공급업체 이메일로 발주서 HTML 메일 발송
  · 메일 실패해도 상태는 변경 (mailSent/mailError 응답)

[매뉴얼]
- 다-1 매입 발주 단계별 가이드 재작성
- "공급업체별 품목 일괄 불러오기" 팁 추가

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 22:26:21 +09:00
chpark 99565bf6e0 feat(v0.7 round1): 공급업체 명칭 변경 + 품목-공급업체 연결 + 거래처 출고이력 거래명세표 모달
Deploy momo-erp / deploy (push) Successful in 55s
[DB]
- 016: momo_items.vendor_objid 추가, momo_vendors 컬럼 보강 (email/address/memo/regdate)
- 017: 메뉴 9000202 "매입처 관리" → "공급업체 관리"

[명칭 일괄 변경]
- src/app/api/m/vendors/* + (main)/m/admin/vendors/* + procurements/* + inbounds/*
- 모든 UI/메시지의 '매입처' → '공급업체'

[품목 ↔ 공급업체 연결]
- /api/m/items/list 응답에 VENDOR_OBJID/VENDOR_NAME 추가, vendorObjid 필터 지원
- /api/m/items/save: vendorObjid 입력/저장 (insert + update)
- 품목 등록·수정 폼에 [공급업체] 드롭다운 신설 (제조사 옆)

[/m/orders 거래처 출고 이력 화면 — 모달 + 이미지 공유]
- 행 클릭 / [보기] 버튼 → 거래명세표 모달
- 모달 안에 [📤 이미지 공유] [⬇ 엑셀 다운로드] 버튼 (출고/정산 화면과 동일)
- 출고요청 상태이면 [🗑 주문 취소] 버튼 노출 → /api/m/orders/cancel
- html-to-image 로 PNG 캡처 → Web Share API 또는 다운로드

[매뉴얼]
- 공급업체 명칭 반영, 출고이력 거래명세표 보기 동작 추가, 품목 폼에 공급업체 필드 설명 추가

Round 2 예정: 매입 발주 양식 (좌측 리스트 + 우측 발주서 + 품목 검색/공급업체 일괄 불러오기) + 매뉴얼 보강

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 22:19:08 +09:00
chpark 8c89c44b5f feat(거래명세표): 공급자 정보 박스 + 비고 컬럼 + 이미지 공유 + 엑셀 수정
Deploy momo-erp / deploy (push) Successful in 1m13s
[공급자 정보 박스 (우측 상단)]
- 결제계좌번호 / 전화번호 / 이메일 표를 거래명세표 우측 상단에 표시
- 환경변수: MOMO_BANK_ACCOUNT / MOMO_PHONE / MOMO_EMAIL / MOMO_COMPANY_CEO 등
- detail API 응답에 supplier 객체 추가

[비고(remark) 컬럼]
- 모든 라인(품목/택배/용차)에 비고 입력 가능
- /api/m/orders/items/remark 신설 — REQUESTED 상태에서만 본인/관리자 수정
- 인풋에서 포커스 이탈/엔터 시 자동 저장
- 모든 라인에 momo_order_items.remark 컬럼 활용 (이미 존재)

[이미지 공유 + 인쇄]
- 거래명세표 위쪽에 [📤 이미지 공유] [🖨 인쇄] 버튼 신설
- html-to-image 라이브러리로 PNG 캡처 → Web Share API 가 있으면 카톡/메신저로 직접 공유,
  없으면 PNG 파일 다운로드 (모바일/PC 호환)
- statementRef 로 캡처 영역 분리 (버튼은 영역 밖)

[엑셀 다운로드 수정]
- 기존: SELECT 쿼리에 alias 빠져 있어(`U.user_name, NULL, NULL`) 회사명/대표자/사업자번호가 모두 빈 값
- 수정: company_name/ceo_name/biz_no/phone/address/email 명시 alias
- 택배/용차 라인은 [택배]/[용차] 라벨로 출력

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:51:30 +09:00
chpark 6b751e48d0 chore(manual + fix): 매뉴얼 한글화 + 택배/용차 라인 동기화 버그 수정
Deploy momo-erp / deploy (push) Successful in 51s
[매뉴얼]
- 영어/기술용어/환경변수 코드 노출 제거
- 초등학생 수준 친절한 설명체로 전면 재작성
- 역할별 시나리오 박스 + 화면 도식 + FAQ 한글 위주
- URL/코드 참조 최소화

[ExtraRow 버그]
- 거래명세표에서 [+ 택배/용차 추가] 클릭해도 인풋 칸의 수량이 화면에 안 바뀌던 문제
- 같은 OBJID 라서 컴포넌트가 unmount 안 되어 useState 초기값 무시되던 케이스
- useEffect 로 line prop 변경 시 인풋 state 동기화

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:45:07 +09:00
chpark fb6e0d38b6 fix(middleware): /manual.html 등 정적 .html/.pdf 파일 인증 면제
Deploy momo-erp / deploy (push) Successful in 51s
증상: https://momotogether.com/manual.html 접속 시 /login 으로 307 리다이렉트.
원인: middleware 가 .html 확장자 정적 파일을 인증 체크 대상으로 분류.
해결: 정적 자산 정규식에 html/htm/pdf/txt 추가.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:30:56 +09:00
chpark b48d3a505f docs: 사용자 매뉴얼 HTML 추가 (public/manual.html)
Deploy momo-erp / deploy (push) Successful in 50s
URL: https://momotogether.com/manual.html
- 거래처(USER) 가이드: 회원가입 / 출고 요청 / 발주 이력 / 회원정보 수정
- 관리자(ADMIN) 가이드: 발주서 관리·출고처리·거래명세표·인라인 편집·세금계산서 발행
- 마스터 관리: 품목·거래처·매입처·창고·제조사
- 매입/입고: 매입 발주·입고·재고
- 통계: 대시보드·월간/일자별/원가마진 + 엑셀 다운로드
- 전체 업무 흐름도
- FAQ: 택배 라인 / 권한 / 메일 / STUB / 모바일

단일 HTML 파일 — 인쇄 가능, 모바일 대응, 외부 폰트 X

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:29:24 +09:00
chpark 57bb74dc14 chore(header): 모바일 친화 — 상단 메뉴 탭/Admin/알람·매뉴얼·테마 아이콘 제거
Deploy momo-erp / deploy (push) Successful in 51s
배경: 모바일에서 헤더가 너무 빽빽함. 사용되지 않는 요소 정리.

제거:
- [사용자] / [관리자] 등 상단 메뉴 탭 (좌측 메뉴로 충분)
- 홈 아이콘
- 알람 (결재함 배지) — MOMO에서는 결재 흐름 X
- 매뉴얼 (BookOpen) 아이콘
- 테마 토글 (Sun/Moon)
- Admin 패널 진입 버튼

유지:
- 사용자명(클릭 시 /profile)
- 로그아웃

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:12:48 +09:00
chpark 70736bd8f6 fix(momo orders): 택배/용차 추가 버튼 — 같은 종류 라인 있으면 수량 +1 (중복 생성 방지)
Deploy momo-erp / deploy (push) Successful in 50s
증상: + 택배 추가 / + 용차 추가 버튼을 누를 때마다 같은 종류 라인이 새로 생성됨.
변경: 같은 kind 의 라인이 이미 있으면 그 라인의 qty 를 +1, 없으면 신규 추가.

- /m/admin/orders (관리자 거래명세표): 기존 라인 찾아 lines/save 로 qty+1 업데이트
- /m/orders/new (거래처 발주 작성): extras 배열에서 같은 kind 라인의 qty +1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:09:16 +09:00
chpark 63d778cfe5 feat(momo v0.6): 대메뉴 순서 재배치 + 모든 로그인 랜딩 → 출고 요청
Deploy momo-erp / deploy (push) Successful in 51s
[메뉴 015]
- 600 거래처 주문 (9000100)
- 650 출고/정산  (9000400)
- 700 매입/입고  (9000300)
- 750 마스터 관리 (9000200)
- 800 통계       (9000500) — 대시보드 자식 포함

[로그인 랜딩]
- 역할 분기 제거. 관리자/거래처 모두 /m/orders/new 로
- 관리자는 좌측 메뉴에서 [출고/정산]으로 이동

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:07:25 +09:00
chpark b0808b1d0a feat(momo v0.6): 거래명세표 행 순서 + 메뉴 재배치 + 로그인 랜딩 변경
Deploy momo-erp / deploy (push) Successful in 51s
[거래명세표 행 순서]
- 택배(DELIVERY)/용차(CHARTER) 라인이 품목(ITEM) 위로 표시되도록 정렬
- /api/m/orders/detail, /api/m/orders/statement/[id]: ORDER BY CASE kind 추가
- /m/admin/orders 화면 + xlsx 출력: 표시 순서 기준으로 SEQ 재부여 (DB seq 와 무관)

[메뉴 014]
- 마스터 관리 (9000200) → 마지막 (seq 900)
- 대시보드 (9000001) → 통계 그룹(9000500) 자식으로 이동, parent 변경
- 빈 [DASHBOARD] 대메뉴(1837127121) 비활성화
- 최종 순서: 거래처 주문 → 매입/입고 → 출고/정산 → 통계(대시보드 포함) → 마스터 관리

[로그인 랜딩]
- 기존: 모든 사용자 /m/dashboard
- 변경: 역할별 분기
  · ADMIN/관리자 → /m/admin/orders (발주서 관리·출고처리)
  · USER/거래처   → /m/orders/new (출고 요청)
- 회원가입 직후도 /m/orders/new 로

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 16:43:57 +09:00
chpark e65ea43429 feat(momo v0.6): 전자세금계산서 메뉴 등록 + 거래명세표에 발행 버튼 추가
Deploy momo-erp / deploy (push) Successful in 51s
[메뉴 마이그레이션 013]
- objid=9000404 '전자세금계산서' (parent=9000400 출고/정산)
- url: /m/admin/einvoices, seq=13

[거래명세표 [세금계산서 발행] 버튼]
- 관리자 발주 상세(/m/admin/orders) 거래명세표 미리보기 하단
- 출고완료(APPROVED/SHIPPED) 또는 입금완료(PAID) 상태에서 노출
- [세금계산서 발행] (과세 TAX) / [계산서(면세)] (TAXFREE) 두 버튼 분리
- 클릭 → 확인 모달 → /api/m/einvoices/issue 호출 → 결과 모달 (승인번호/처리방식 표시)
- 발행 후 같은 화면에 "세금계산서 발행됨 (승인번호)" 표시

[현재 흐름 (v0.6)]
1. 거래처: 출고 요청
2. 담당자: 체크 + [출고] 버튼 → 재고 차감 + 거래명세표 메일 자동 발송 (status=APPROVED)
3. 담당자: 거래명세표에서 [세금계산서 발행] 버튼 클릭 → 전자세금계산서 발행
4. 발행 이력은 /m/admin/einvoices 메뉴에서 일괄 조회/엑셀 다운로드

[추후 옵션]
- 출고 처리 시 자동 세금계산서 발행 토글 (지금은 명시 발행만)
- nts-esero 어댑터 실 통신 활성화 (인증서 + ERP 연계 승인 후)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 16:28:07 +09:00
chpark 0b0749cfb1 chore: package-lock.json normalize (npm install 부산물)
Deploy momo-erp / deploy (push) Successful in 1m14s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 16:19:17 +09:00
chpark 7e764d500e feat(momo v0.6): 전자세금계산서 발행 모듈 골격 (별도 메뉴 + 국세청 직접 연동 준비)
Deploy momo-erp / deploy (push) Successful in 51s
[정책]
- 발주/출고/입금 흐름과 분리된 별도 메뉴 (월말 일괄 또는 신고 시점 발행 가능)
- 출고 시 자동 발행은 향후 토글 옵션으로 추가

[DB 012]
- momo_einvoices: 발행 이력 (공급자/받는자/금액/승인번호/상태/원본XML)
- momo_einvoice_items: 라인별 상세
- 상태: DRAFT → QUEUED → SENT → ACK | FAIL | CANCELED

[발행 어댑터 추상화 (lib/einvoice)]
- InvoiceProvider 인터페이스 — issue/status/cancel
- adapters/manual.ts: 자체 거래명세서 (국세청 전송 X, 기본)
- adapters/nts-esero.ts: 국세청 e-세로 직접 연동 골격
  · NTS_ESERO_MODE: stub | test | prod
  · stub 모드는 DB 기록만 (개발/CI 안전)
  · 실 통신은 사업자 공동인증서 + ERP 연계 승인 후 활성화
  · SOAP/XMLDSig 페이로드 빌더 골격 작성, 인증서 받으면 서명+전송 추가
- index.ts: EINVOICE_PROVIDER 환경변수로 어댑터 선택

[API]
- POST /api/m/einvoices/list: 발행 이력 조회 + 필터 (관리자)
- POST /api/m/einvoices/issue: 발주(orderObjid)로부터 또는 수동 입력으로 발행
  · 어댑터 결과를 momo_einvoices/_items 에 트랜잭션 기록 (성공/실패 모두)

[UI]
- /m/admin/einvoices 페이지 신설
  · 발행 가능 발주 리스트 (출고/입금 완료된 건)
  · 한 번 클릭으로 세금계산서 발행 → 결과 모달
  · 발행 이력 (날짜/상태/승인번호 필터, 엑셀 다운로드)
  · STUB 모드 안내 배너 — 운영 활성화 절차 명시

[문서]
- docs/MOMO_DISTRIBUTION_SPEC.md 부록 B (v0.6) 추가

다음 단계 (인증서 + ERP 연계 승인 후):
- nts-esero.ts 의 SOAP + XMLDSig 실제 구현
- NTS_ESERO_MODE=test 로 100건 검증
- NTS_ESERO_MODE=prod 전환

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 16:14:02 +09:00
chpark a336191153 fix(db 011): momo_order_items.item_objid NOT NULL → NULLABLE
Deploy momo-erp / deploy (push) Successful in 50s
증상: 거래명세표에서 [+ 택배/용차 추가] 클릭 시
  null value in column "item_objid" of relation "momo_order_items" violates not-null constraint

원인: 001_momo_init.sql 에서 item_objid 가 TEXT NOT NULL 로 정의됨.
       택배/용차 라인(kind=DELIVERY/CHARTER)은 품목이 아니라 가상 부가 라인이라 NULL 이 정상.

해결: ALTER ... DROP NOT NULL. ITEM 라인은 어차피 코드 레벨에서 항상 값을 넣고 있어 무영향.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:59:24 +09:00
chpark 194dfffae1 fix(deploy): 마이그레이션 호스트 폴백 + 라인 저장 에러 메시지 상세화
Deploy momo-erp / deploy (push) Successful in 52s
증상: /m/admin/orders 거래명세표에서 [+ 택배/용차 추가] 클릭 시 "라인 저장 중 오류" 500.
원인: 마이그레이션 010 (momo_order_items.kind/extra_label, momo_orders.total_delivery/charter)이
       운영 DB에 적용되지 않음. deploy.yml 의 docker compose exec 가 silent fail 했을 가능성.

[deploy.yml]
- 컨테이너 안 마이그레이션 실패 시 → 호스트에서 docker run node:20 + pg 임시 컨테이너 폴백
- 호스트는 source 디렉토리를 /work 로 마운트해서 db/migrations 와 scripts/migrate-momo.mjs 직접 실행
- 모두 실패하면 ::error:: 로 명확히 표시 (warning 무시 방지)

[lines/save]
- catch 블록에서 실제 PG 에러 메시지 노출
- 'column does not exist'(42703) 감지 시 마이그레이션 미적용 힌트 추가

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:53:28 +09:00
chpark c9b375bbcb feat(momo v0.5): 거래명세표에서 택배/용차 라인 직접 추가/수정/삭제
Deploy momo-erp / deploy (push) Successful in 1m14s
[요구사항 반영]
- 택배비/용차비는 매번 달라지므로 수기 입력 (담당자명+단가+수량)
- 용차 추가 버튼 → 한 라인 생성, 기본 단가 5000원
- 단가×수량 = 합계 자동 계산 (예: 5000 × 6 = 30,000)
- 거래명세표(관리자 발주 상세)에서 바로 수정 가능

[API]
- /api/m/orders/save extras 입력 형태 변경: amount → unitPrice + qty (amount 호환 유지)
- /api/m/orders/lines/save 신설: REQUESTED 상태 발주에 택배/용차 라인 추가/수정/삭제
  · 본인 또는 관리자만 가능, ITEM 라인은 보호됨
  · 라인 변경 후 momo_orders 합계 컬럼 (total_supply/vat/amount/taxfree/taxable/delivery/charter) 자동 재집계

[UI]
- /m/orders/new (거래처 발주 작성):
  · 택배/용차 라인 단가+수량 분리 입력, 기본 단가 4000(택배)/5000(용차)
  · 라인별 합계 실시간 표시 (단가 × 수량 = 합계)
- /m/admin/orders (관리자 거래명세표):
  · REQUESTED 상태에서 [+ 택배 추가] [+ 용차 추가] 버튼 노출
  · 택배/용차 라인은 인라인 편집 (담당자명/단가/수량 즉시 수정, 체크 클릭 → 저장)
  · 삭제는 X 버튼

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:27:03 +09:00
2506 changed files with 20295 additions and 1413386 deletions
+1 -9
View File
@@ -1,4 +1,4 @@
DATABASE_URL="postgresql://momo_app:qlalfqjsgh11@183.99.177.40:5432/distribution"
DATABASE_URL="postgresql://momo_app:qlalfqjsgh11@121.156.99.3:5432/distribution"
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="2b1f94cca798f49ff62822b01617503b019d118df9d249ee61f835a7dca1946e"
NEXT_PUBLIC_APP_NAME="유통관리 ERP"
@@ -17,11 +17,3 @@ SMTP_FROM=모모유통 <chpark@coa-soft.com>
MOMO_BANK_ACCOUNT=기업은행 434-115361-01-016
MOMO_PHONE=010-6624-5315
DEPLOY_WEBHOOK_TOKEN=momo-deploy-2026-secure
# 카카오 OAuth (개발) — 카카오 디벨로퍼스에서 발급 후 채워넣기
# https://developers.kakao.com/console/app → 앱 키 → REST API 키
# 카카오 로그인 → 활성화 ON, Redirect URI 등록 필수
# 동의항목: 이메일(필수 동의), 닉네임(기본)
KAKAO_REST_API_KEY=1e7825e926c4e8de575ee73ecbd02398
KAKAO_CLIENT_SECRET=jva60F8UfxZtDFIYqZsbECeHl8fcz6e6
KAKAO_REDIRECT_URI=http://localhost:3000/api/auth/kakao/callback
+7
View File
@@ -17,3 +17,10 @@ SMTP_FROM=모모유통 <momo8443@daum.net>
# ============ 거래명세표에 표시될 공급자 정보 ============
MOMO_BANK_ACCOUNT=기업은행 434-115361-01-016
MOMO_PHONE=010-6624-5315
# ============ 웹 푸시(PWA 알림) ============
# 미설정 시 코드 하드코딩 기본 VAPID 키 사용. 운영에서 키를 교체하려면 아래 지정.
# 생성: npx web-push generate-vapid-keys
# VAPID_PUBLIC_KEY=__public__
# VAPID_PRIVATE_KEY=__private__
# VAPID_SUBJECT=mailto:admin@momotogether.com
-83
View File
@@ -1,83 +0,0 @@
name: Deploy momo-erp
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Install sshpass
run: |
apt-get update -qq && apt-get install -y -qq sshpass openssh-client || \
sudo apt-get update -qq && sudo apt-get install -y -qq sshpass openssh-client
- name: Deploy via SSH (password auth)
run: |
set +e # 배포 단계 실패해도 워크플로우 성공 처리 (실제 결과는 헬스체크가 판단)
export SSHPASS='qlalfqjsgh11'
mkdir -p ~/.ssh
ssh-keyscan -H 183.99.177.40 >> ~/.ssh/known_hosts 2>/dev/null || true
sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
chpark@183.99.177.40 'bash -s' <<'REMOTE_SCRIPT'
set +e
DEPLOY_DIR="$HOME/momo-erp/source"
mkdir -p "$HOME/momo-erp"
if [ -d "$DEPLOY_DIR/.git" ]; then
cd "$DEPLOY_DIR"
git fetch origin
git reset --hard origin/main
else
git clone https://git.junggomoa.com/chpark/distribution_erp.git "$DEPLOY_DIR"
cd "$DEPLOY_DIR"
fi
# .env.production 갱신 (SMTP/MOMO 포함)
cat > .env.production <<'ENVEOF'
DATABASE_URL=postgresql://momo_app:qlalfqjsgh11@183.99.177.40:5432/distribution
NEXTAUTH_URL=https://momotogether.com
NEXTAUTH_SECRET=2b1f94cca798f49ff62822b01617503b019d118df9d249ee61f835a7dca1946e
NEXT_PUBLIC_APP_NAME=유통관리 ERP
NEXT_PUBLIC_COMPANY_NAME=모모유통
MASTER_PWD=qlalfqjsgh11
AES_KEY=ILJIAESSECRETKEY
FILE_STORAGE_PATH=/data_storage
LOG_LEVEL=info
SMTP_HOST=mail.coa-soft.com
SMTP_PORT=465
SMTP_USER=chpark@coa-soft.com
SMTP_PASS=1321Qkrckd!!!!!!
SMTP_FROM=모모유통 <chpark@coa-soft.com>
MOMO_BANK_ACCOUNT=기업은행 434-115361-01-016
MOMO_PHONE=010-6624-5315
DEPLOY_WEBHOOK_TOKEN=momo-deploy-2026-secure
ENVEOF
docker compose -f docker-compose.prod.yml up -d --build
# 마이그레이션 (idempotent)
docker compose -f docker-compose.prod.yml exec -T momo-erp npm run migrate:momo || \
echo "::warning::migration skipped"
docker compose -f docker-compose.prod.yml ps
echo "✔ 배포 완료"
REMOTE_SCRIPT
- name: Healthcheck (실제 배포 성공 판정)
run: |
for i in 1 2 3 4 5 6 7 8 9 10; do
sleep 10
CODE=$(curl -s -o /tmp/p.html -w "%{http_code}" -L https://momotogether.com/ || echo 000)
HAS_NEW=$(grep -q "WORKFLOW\|매입 발주\|SCREEN PREVIEW" /tmp/p.html && echo yes || echo no)
echo " ${i}/10: HTTP $CODE / 신버전=$HAS_NEW"
if [ "$CODE" = "200" ] && [ "$HAS_NEW" = "yes" ]; then
echo "::notice::✔ 운영 정상 + 신버전 코드 확인"
exit 0
fi
done
echo "::error::헬스체크 실패: 신버전 코드가 운영에 반영되지 않음"
exit 1
+15
View File
@@ -24,6 +24,13 @@
.DS_Store
*.pem
# 안드로이드 서명 키/번들 — 절대 커밋 금지 (Play Store 서명 키 노출 = 앱 도용 위험)
*.keystore
*.jks
*.aab
*.apk
signing-key-info.txt
# debug
npm-debug.log*
yarn-debug.log*
@@ -42,3 +49,11 @@ yarn-error.log*
next-env.d.ts
/src/generated/prisma
# Android TWA build artifacts / keystore (절대 커밋 금지)
android/*.keystore
android/*.jks
android/*.aab
android/*.apk
android/app/
android/.gradle/
+5 -5
View File
@@ -3,7 +3,7 @@
## 개요
`.gitea/workflows/deploy.yml` 워크플로가 `main` 브랜치 푸시 시 자동으로
배포 서버(183.99.177.40)에 SSH 접속 → `docker compose up -d --build` 실행합니다.
배포 서버(121.156.99.3)에 SSH 접속 → `docker compose up -d --build` 실행합니다.
## Gitea 시크릿 등록
@@ -11,10 +11,10 @@ Gitea 저장소 → **Settings → Actions → Secrets** 에 다음 시크릿을
| 시크릿 이름 | 값 (예시) |
|-------------|----------|
| `DEPLOY_HOST` | `183.99.177.40` |
| `DEPLOY_HOST` | `121.156.99.3` |
| `DEPLOY_USER` | `chpark` |
| `DEPLOY_SSH_KEY` | SSH 개인키 전체 (BEGIN/END 포함) |
| `DATABASE_URL` | `postgresql://postgres:qlalfqjsgh11@183.99.177.40:5432/distribution` |
| `DATABASE_URL` | `postgresql://momo_app:qlalfqjsgh11@121.156.99.3:5432/distribution` |
| `NEXTAUTH_URL` | `https://momotogether.com` |
| `NEXTAUTH_SECRET` | 임의의 32바이트 hex (현재 .env.production 값 재사용 가능) |
| `MASTER_PWD` | `qlalfqjsgh11` |
@@ -26,7 +26,7 @@ Gitea 저장소 → **Settings → Actions → Secrets** 에 다음 시크릿을
```bash
ssh-keygen -t ed25519 -C "gitea-deploy" -f ~/.ssh/momo_deploy -N ""
# 공개키를 배포 서버에 등록
ssh-copy-id -i ~/.ssh/momo_deploy.pub chpark@183.99.177.40
ssh-copy-id -i ~/.ssh/momo_deploy.pub chpark@121.156.99.3
# 개인키를 Gitea Secret `DEPLOY_SSH_KEY` 에 붙여넣기
cat ~/.ssh/momo_deploy
```
@@ -41,7 +41,7 @@ cat ~/.ssh/momo_deploy
긴급 시:
```bash
ssh chpark@183.99.177.40
ssh chpark@121.156.99.3
cd ~/momo-erp/source
git pull
docker compose -f docker-compose.prod.yml up -d --build
+2 -2
View File
@@ -26,7 +26,7 @@
## 기술 스택
- **Frontend**: Next.js 15 (App Router), React 19, TypeScript, Tailwind CSS
- **Backend**: Next.js API Routes (Node.js)
- **Database**: PostgreSQL (외부 공용 서버 `211.115.91.141:11140/fito`, raw SQL via `pg`)
- **Database**: PostgreSQL (외부 공용 서버 `121.156.99.3:5432/distribution`, raw SQL via `pg`)
- **인증**: JWT (jose) + Cookie 기반 세션
- **상태관리**: Zustand
- **UI**: SweetAlert2, Lucide Icons, Custom DataGrid (TanStack React Table)
@@ -76,4 +76,4 @@ npm run dev # 개발 서버 (localhost:3000)
- Docker Compose dev/prod 분리
- Traefik 리버스 프록시 + `fito.wace.me` 서브도메인
- DB는 외부 `211.115.91.141:11140/fito` 공유 (컨테이너 내부 DB 없음)
- DB는 외부 `121.156.99.3:5432/distribution` 공유 (컨테이너 내부 DB 없음)
+8 -1
View File
@@ -26,9 +26,16 @@ ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
# webhook 자기재배포에 필요한 CLI: git (소스 동기), docker + compose (이미지 빌드/swap)
# docker socket 은 docker-compose.prod.yml 에서 host 의 /var/run/docker.sock 으로 마운트됨
RUN apk add --no-cache git docker-cli docker-cli-compose
# 비루트 사용자 (보안)
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
adduser --system --uid 1001 nextjs && \
# docker socket 접근 권한: 운영 호스트 /var/run/docker.sock 의 GID(988) 와 동일한 그룹 생성 후 nextjs 가입
addgroup -g 988 dockerhost && \
addgroup nextjs dockerhost
# standalone 번들 복사
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
+320
View File
@@ -0,0 +1,320 @@
# 인프라 이관 가이드 (집 PC → IDC)
> 작성: 2026-05-12 / 대상 호스트: `chparkserver` (183.99.177.40)
> 목적: 운영 중인 서비스 전부 IDC 서버로 옮길 때 필요한 정보와 절차 정리
---
## 1. 현재 호스트 사양 / 환경
| 항목 | 값 |
|---|---|
| Hostname | `chparkserver` |
| OS | Ubuntu 24.04.4 LTS (kernel 6.8) |
| Docker | 29.1.3 |
| K3s | v1.34.6+k3s1 |
| 디스크 | `/` 116G / `/data` 916G (사용 121G) |
| 외부 IP | 183.99.177.40 |
| 접속 | SSH (chpark@) |
**디스크 사용량 분포 (`/data`)**:
```
51G containerd (K3s container runtime)
31G docker (docker engine data-root)
56G opt (docker compose 운영 파일들)
544M transfer
490M dumps
303M k3s
90M mailu
87M backups ← 이번 사고 대응으로 만든 백업
```
---
## 2. 운영 중인 서비스 전체 목록
### 2.1 Docker Compose 기반 (직접 운영)
| Project | 경로 | 도메인/포트 | 비고 |
|---|---|---|---|
| **traefik** | `/data/opt/docker/traefik/` | 80, 443 | HTTPS reverse proxy (Let's Encrypt). 모든 도메인 입구 |
| **mailufinal** | `/data/mailu/` | mail.coa-soft.com (25/110/143/465/587/993/995) | Mailu 메일 서버 9개 컨테이너 |
| **nextcloudfinal** | `/home/chpark/nextcloud/` | cloud.junggomoa.com | Nextcloud + MariaDB 11.8 + Redis |
| **gitea2** | `/data/opt/docker/gitea/` | git.junggomoa.com / 2222 (SSH) | Gitea + PostgreSQL |
| **mattermost2** | `/data/opt/docker/mattermost/` | 8065 | Mattermost + PostgreSQL |
| **registry2** | `/data/opt/docker/registry/` | localhost:5000 | Docker registry (K3s 가 image pull 용) |
| **source** | `/home/chpark/momo-erp/source/` | momotogether.com | momo-erp |
| **tradeing2** | `/home/chpark/tradeing/` | (도메인 확인) | Tradeing 서비스 + PostgreSQL |
| **portainer** | `/data/opt/docker/portainer/` | 9000 (관리 UI) | Docker 관리 UI |
> ⚠️ **stopped 상태로 남은 옛 프로젝트**: `gitea`, `mailu`, `mattermost`, `nextcloud`, `registry`, `tradeing` — 이전 버전. **이관 시 무시**.
### 2.2 K3s 위에서 운영 (kubectl 로 관리)
| Namespace | 서비스 | 도메인 |
|---|---|---|
| `invyone` | frontend / backend-spring / backend-node | www.invyone.com / solution.invyone.com |
| `invyone-homepage` | homepage | (invyone homepage) |
| `insurance` | api / web / postgres-0 | (insurance 도메인) |
| `kubernetes-dashboard` | dashboard / metrics-scraper | (관리용) |
| `portainer` | portainer-agent | (관리용) |
| `kube-system` | coredns / metrics-server / local-path-provisioner | (인프라) |
> K3s manifests: `/data/k8s-manifests/` 또는 Gitea 의 repo 확인.
---
## 3. 데이터 볼륨 (이관 시 반드시 옮길 것)
### 3.1 Docker Named Volumes
```
nextcloud_nextcloud_db ← Nextcloud DB (MariaDB 11.8)
nextcloud_nextcloud_data ← Nextcloud 사용자 파일
insurance_postgres_data ← Insurance K3s PostgreSQL
insurance_uploads ← Insurance 업로드 파일
invyone-db-data ← Invyone PostgreSQL
source_momo_data_storage ← Momo ERP 데이터
```
호스트 경로: `/data/docker/volumes/<volume_name>/_data/`
### 3.2 Bind Mount 경로
```
/data/mailu/ ← Mailu compose + env + 인증서
/mailu/ ← Mailu data (메일함 실제 저장 위치, /mailu/data)
/data/opt/docker/ ← 각 서비스의 compose 디렉토리
/home/chpark/nextcloud/ ← Nextcloud compose
/home/chpark/momo-erp/ ← Momo ERP compose + source
/home/chpark/tradeing/ ← Tradeing compose
```
### 3.3 K3s/containerd
```
/data/containerd/ ← K3s container runtime data (이관 비추, K3s 새로 설치 + manifest 재배포 권장)
/data/k3s/ ← K3s state
/data/k8s-manifests/ ← K8s manifest 파일들 (직접 이관)
```
---
## 4. 외부 노출 포트 (방화벽 규칙)
| 포트 | 프로토콜 | 용도 |
|---|---|---|
| 80, 443 | TCP | Traefik (HTTPS 리버스 프록시) |
| 25 | TCP | SMTP (메일 수신) |
| 465 | TCP | SMTPS (메일 송신 TLS) |
| 587 | TCP | SMTP Submission |
| 993 | TCP | IMAPS |
| 995 | TCP | POP3S |
| 110, 143 | TCP | POP3/IMAP plain (보안상 disable 권장) |
| 2222 | TCP | Gitea SSH |
| 8065 | TCP | Mattermost |
| 5432 | TCP | **⚠️ invyone-db PostgreSQL 외부 노출** — IDC 이관 시 내부 IP 만 허용하도록 변경 권장 |
---
## 5. 운영 중 적용된 Hotfix 내역 (이관 시 그대로 가져가야 함)
이관 시 단순히 compose 파일만 복사하면 안 됨. 아래는 컨테이너/네트워크 quirk 대응으로 추가한 패치.
### 5.1 Mailu (`/data/mailu/docker-compose.yml`)
- **admin / front / antispam** 의 `entrypoint` override:
```yaml
entrypoint:
- /bin/sh
- -c
- |
echo "nameserver 127.0.0.11" > /etc/resolv.conf
echo "nameserver 192.168.203.2" >> /etc/resolv.conf
echo "search ." >> /etc/resolv.conf
echo "options edns0 trust-ad ndots:0" >> /etc/resolv.conf
exec /start.py
```
- **admin / front / antispam** 에 `extra_hosts` (mailu 내부 service IP 직접 박음):
```yaml
extra_hosts:
- "redis:192.168.203.4"
- "imap:192.168.203.9"
- "smtp:192.168.203.6"
- "antispam:192.168.203.8"
- "webmail:192.168.203.10"
- "front:192.168.203.3"
- "resolver:192.168.203.2"
```
> 이유: docker user-defined bridge network 에서 admin 만 DNSSEC AD flag 강제. embedded DNS(127.0.0.11) 가 DNSSEC 안 함 → admin 시작 실패. mailu unbound resolver(192.168.203.2) 가 DNSSEC capable 이라 그쪽으로 박았고, internal service hostname 은 `extra_hosts` 로 박아 NXDOMAIN 방지.
- **resolver** service 의 `ipv4_address: 192.168.203.254` **제거됨** (자동 할당).
> 이유: 192.168.203.254 IP 가 packet drop 되는 docker bridge quirk 발견. 자동 할당으로 .2 받음.
- **`/data/mailu/mailu.env`** 에 추가:
```
WEBROOT_REDIRECT=/webmail/
```
> 이유: 로그인 후 admin 계정이라 가끔 admin UI 로 redirect 됨. 항상 webmail 로 일관시킴.
### 5.2 Nextcloud (`/home/chpark/nextcloud/docker-compose.yml`)
- DB 컨테이너 image: `mariadb:10.5` → **`mariadb:11.8`** (원본 데이터 버전 일치)
- `command:` 라인 중복 제거 (이전엔 두 줄이라 첫 번째가 덮어써짐)
- 백업 위치: `/data/backups/nextcloud_db_20260512_080401.tgz` (87MB)
- 추가 mariadb-dump: `/data/backups/nc_db_*.sql`
### 5.3 Docker daemon (`/etc/docker/daemon.json`)
- 사용자가 이전에 추가한 `"dns": ["8.8.8.8", "1.1.1.1"]` 가 모든 컨테이너 dns 옵션과 합쳐져 충돌. **이관 시 daemon.json 의 dns 키는 빼는 게 안전** (Mailu 가 의존하는 docker embedded DNS 만 사용하도록).
### 5.4 cron
```
*/5 * * * * /mailu/sync-certs.sh
0 4 * * * /usr/bin/docker builder prune -af --filter until=72h
5 4 * * * /usr/bin/docker image prune -af --filter until=168h
```
이관 시 root crontab 그대로 복사. `sync-certs.sh` 는 traefik 발급 인증서를 Mailu 형식으로 변환하는 스크립트.
---
## 6. 이관 절차 (체크리스트)
### Phase 1 — 신규 IDC 호스트 준비
- [ ] Ubuntu 24.04 LTS 설치 (현재와 동일 버전 권장)
- [ ] Docker 29.x + Docker Compose v2 설치
- [ ] K3s 설치 (`curl -sfL https://get.k3s.io | sh -`)
- [ ] 디스크 파티션: `/` (시스템) + `/data` (운영 데이터, **916G 이상 권장**)
- [ ] systemd: `docker`, `k3s`, `containerd` 자동 시작 enable
### Phase 2 — 데이터 백업 (현재 호스트)
```bash
# 1. 모든 docker compose 정지 (downtime 시작)
for proj in mailufinal nextcloudfinal gitea2 mattermost2 registry2 source tradeing2 traefik portainer; do
docker compose -p $proj down
done
# 2. K3s 도 정지 (시작 시 컨테이너 자동 stop)
sudo systemctl stop k3s
# 3. volume + 운영 디렉토리 통째로 백업
sudo tar -czf /data/backups/all_volumes_$(date +%Y%m%d).tgz \
/data/docker/volumes \
/data/mailu \
/data/opt \
/data/k8s-manifests \
/home/chpark/nextcloud \
/home/chpark/momo-erp \
/home/chpark/tradeing \
/mailu
# 4. crontab 백업
sudo crontab -l > /data/backups/root_crontab.txt
# 5. daemon.json 백업
sudo cp /etc/docker/daemon.json /data/backups/daemon.json
```
### Phase 3 — IDC 로 전송
```bash
# rsync 권장 (재시도 가능)
rsync -avzP --partial --append-verify \
/data/backups/all_volumes_*.tgz \
user@idc-host:/data/backups/
```
### Phase 4 — IDC 에서 복원
```bash
cd /
sudo tar -xzf /data/backups/all_volumes_*.tgz
# crontab 복원
sudo crontab /data/backups/root_crontab.txt
# 각 서비스 기동
for path in /data/opt/docker/traefik /data/mailu /home/chpark/nextcloud /data/opt/docker/gitea /data/opt/docker/mattermost /data/opt/docker/registry /home/chpark/momo-erp/source /home/chpark/tradeing /data/opt/docker/portainer; do
cd "$path" && docker compose up -d
done
# K3s manifest 재적용
kubectl apply -f /data/k8s-manifests/
```
### Phase 5 — DNS 변경
- 도메인 A 레코드를 새 IDC IP 로 변경 (TTL 짧게 미리 줄여놓으면 좋음)
- `mail.coa-soft.com`
- `cloud.junggomoa.com`
- `git.junggomoa.com`
- `momotogether.com`
- `www.invyone.com` / `solution.invyone.com`
- 기타
- Mailu MX 레코드도 새 IP 로
### Phase 6 — 검증
```bash
# 컨테이너 상태
docker ps --filter "name=mailufinal\|nextcloud\|traefik\|gitea" --format "{{.Names}} {{.Status}}"
# HTTP 확인
for url in https://mail.coa-soft.com/ https://cloud.junggomoa.com/ https://git.junggomoa.com/ https://momotogether.com/; do
curl -ksS -o /dev/null -w "$url -> %{http_code}\n" "$url"
done
# SMTP banner
echo "QUIT" | nc -w 3 새IDC_IP 25
```
---
## 7. 이관 후 정리할 것 (선택)
- [ ] 옛 compose project (gitea/mailu/mattermost/nextcloud/registry/tradeing) 삭제 — `docker compose -p <이름> down --remove-orphans`
- [ ] dangling image 정리 — 이미 cron 으로 자동화됨
- [ ] `/data/backups/` 의 오래된 tarball 정리
- [ ] `daemon.json` 의 `"dns"` 키 제거 (Mailu admin DNS 충돌 방지)
- [ ] Mailu admin/front/antispam 의 entrypoint override 가 더 이상 필요한지 재확인 (IDC 의 docker network 환경에서는 안 필요할 수도)
---
## 8. 핵심 비밀번호 / 인증 정보
> ⚠️ 이 문서에는 비밀번호 직접 안 박는다. 아래 위치에서 확인:
- Mailu 계정: `/data/mailu/mailu.env` (SECRET_KEY, DB_PW 등)
- Nextcloud DB: `/home/chpark/nextcloud/docker-compose.yml` (MYSQL_ROOT_PASSWORD)
- 각 서비스 admin 계정: 해당 compose 의 environment 또는 env_file
- Traefik basic auth: `/data/opt/docker/traefik/` 안 설정
- SSH 비밀번호: 운영 PC 관리자가 보관
이관 시 새 IDC 에서 비밀번호 **모두 회전(rotation) 권장**.
---
## 9. 알려진 이슈 / 주의사항
1. **Mailu resolver 192.168.203.254 IP 회피**: 이번 사고에서 .254 IP 가 packet drop 되는 docker bridge quirk 발견. IDC 에서도 발생할 수 있으니 resolver 는 자동 할당으로 두는 게 안전.
2. **MariaDB 버전 일치**: Nextcloud DB 는 11.8.x. 이관 시 절대 다운그레이드 금지 (이번에 10.5 로 갔다가 plugin load fail 로 사고남).
3. **K3s 와 docker 의 iptables/nftables 공존**: K3s 의 kube-router 가 FORWARD chain 에 끼어들어서 docker bridge 통신에 영향을 줄 수 있음. mailu 의 192.168.203.0/24 subnet 이 다른 서비스의 K3s pod CIDR 와 안 겹치는지 확인.
4. **메일 데이터 위치**: 사용자 메일함 파일은 `/mailu/data/` (호스트 root). `/data/mailu/` 와 다른 위치. 이관 시 둘 다 옮겨야 함.
5. **인증서**: Let's Encrypt 인증서는 Traefik 이 자동 발급. 이관 후 도메인 가리키면 자동 재발급. 단 rate limit 주의 (도메인당 주 5건).
---
## 10. 빠른 참조 — 자주 쓰는 명령
```bash
# 전체 컨테이너 상태
docker ps --format "{{.Names}}\t{{.Status}}" | sort
# 특정 서비스 로그
docker logs --tail 50 mailufinal-admin-1
# 한 서비스만 재기동
cd /data/mailu && docker compose up -d --force-recreate admin
# Mailu 계정 추가 (admin 컨테이너 안)
docker exec mailufinal-admin-1 flask mailu user $USER $DOMAIN $PASSWORD
# Nextcloud occ
docker exec -u www-data nextcloud-app-new php occ status
# K3s pod 상태
kubectl get pods -A
```
+155 -47
View File
@@ -1,70 +1,178 @@
# FITO — (주)피토 PLM (Next.js)
# 모모유통 ERP (Distribution ERP)
기존 Java/Spring MVC + JSP + MyBatis 기반 FITO PLM을 Next.js 15 + Node.js로 컨버전한 시스템.
식자재/도소매 **유통·물류 업무 통합 관리 시스템**.
거래처가 발주를 넣고, 본사가 출고·정산하고, 매입·입고·재고·세금계산서까지 한 화면에서 처리한다.
- 원본: [/Users/jhj/FITO](../FITO) (Java 7 + Spring 3.2.4 + MyBatis 3.2.3 + JSP)
- DB: 외부 PostgreSQL `211.115.91.141:11140/fito` (기존 스키마 그대로 사용)
- 이전 이력: `woosung-nextjs`에서 피벗. 스냅샷 태그 `woosung-v1-snapshot`.
- 운영 도메인: **https://momotogether.com**
- Android 앱: **`com.momotogether.app`** (TWA, Play 스토어 등록용 AAB / 사이드로드 APK)
- 코드 저장소(원격): `git.junggomoa.com/chpark/distribution_erp`
- 사용자: (주)모모유통 본사·김포지사 + 계약 거래처 전용 — 일반 소비자용 아님
## 개발 시작
---
## 주요 기능
### 거래처(구매자) 화면
- **출고 요청** — 재고 있는 품목 선택 → 장바구니 → 발주 요청. 택배전용·용차비·환불 라인 자동/수동 추가. 상시판매·기간한정 품목 분리.
- **내 출고 이력** — 본인 발주 진행상태 추적.
- **푸시 알림 토글** — PWA 설치 후 새 상품 등록 시 알림 수신.
- **회원정보 수정** — 본인 정보·기준 명세표 변경.
### 본사/지사 운영
- **출고 처리** — 발주 검토 → 거래명세표 자동 생성(이미지/엑셀/메일 발송) → 재고 자동 차감.
- **입금 관리** — 출고 후 미수금 추적·입금 등록.
- **계산서·전자세금계산서** — 과세/면세 자동 분리. 국세청 ESERO 연동 어댑터(현재는 DB 기록).
- **매입 발주** — 공급업체별 발주서 작성·메일 발송. 발주지사(HQ/KIMPO) 선택.
- **매입 입금관리** — 진행상태와 결재상태(입금완료/미입금)를 **분리 관리**. 부분입고 시 입고금액 기준 입금 처리.
- **입고 처리** — 정상/불량 분리 입고, 유통기한·물류팀 최종완료자 체크리스트.
- **재고 관리** — 창고별 실시간 재고, 입출고 이력, 창고 간 이동, 유통기한 임박 알림.
### 마스터·통계·관리
- **품목 마스터** — 가격·원가·과세/면세·택배전용·1회 발주한도·판매기간(또는 상시판매) + 일괄 적용.
- **공급업체/거래처/창고/계산서 기준정보** 관리.
- **푸시알림 게시판** — 관리자가 공지(이미지+본문)를 작성하고 선택한 구독자에게 푸시 발송. 사용자가 알림 탭하면 공지 페이지로 이동.
- **통계** — 대시보드, 월간/일자별 매출, 원가·마진, 거래처×일자 매출, 지사 수수료, 창고 이동 통계.
---
## 기술 스택
- **풀스택 단일 Next.js 프로젝트** — 프론트(React 19, App Router) + 백엔드(API Routes, Node.js) 한 저장소.
- **TypeScript** strict mode, **Tailwind CSS**.
- **DB**: 외부 PostgreSQL `121.156.99.3:5432/distribution` — raw SQL(`pg`).
- **인증**: JWT(jose) + HTTP Cookie 세션 + AES-128-ECB(비밀번호).
- **PWA**: `manifest.json` + Service Worker(`public/sw.js`) — 푸시 핸들러·알림 위임(badge·icon).
- **푸시**: `web-push`(VAPID) — `momo_push_subscriptions` 에 endpoint 저장, 발송은 `lib/push.ts`.
- **거래명세표 캡처**: `html-to-image` (이미지 공유/저장).
- **상태관리**: Zustand (auth/menu/theme).
---
## 디렉토리 구조
```
src/
├── app/
│ ├── (auth)/login 로그인
│ ├── (main)/
│ │ ├── m/orders/new 거래처 — 출고요청
│ │ ├── m/orders 거래처 — 내 출고이력
│ │ ├── m/notices/[id] 공지 도달 페이지 (푸시 클릭 시)
│ │ ├── m/admin/orders 출고 처리
│ │ ├── m/admin/payments 입금 관리
│ │ ├── m/admin/invoices 계산서 발행
│ │ ├── m/admin/einvoices 전자세금계산서
│ │ ├── m/admin/procurements매입 발주서
│ │ ├── m/admin/proc-payments 매입 입금관리
│ │ ├── m/admin/inbounds 입고 처리
│ │ ├── m/admin/inventory 재고 관리
│ │ ├── m/admin/items 품목 마스터
│ │ ├── m/admin/notices 푸시알림 게시판
│ │ ├── m/admin/vendors 공급업체 관리
│ │ ├── m/admin/warehouses 창고 관리
│ │ ├── m/admin/statistics 통계 대시보드
│ │ └── profile 회원정보 수정
│ └── api/m/ 업무 API (orders/items/inbounds/push/notices …)
├── lib/
│ ├── db.ts PostgreSQL Pool (queryRows/queryOne/execute)
│ ├── auth.ts 인증 + 세션
│ ├── push.ts web-push 발송 + 구독 관리
│ ├── notices.ts 공지 테이블 자동 생성
│ ├── momo-proc.ts 매입 진행/결재 분리 마이그레이션
│ └── capture-share.ts 이미지 캡처/공유
├── components/
│ ├── layout/ Header / Sidebar
│ ├── grid/ DataGrid (TanStack Table)
│ ├── ui/ 버튼/입력/SearchableSelect 등
│ └── push-optin.tsx 푸시 알림 켜기/끄기 토글 (localStorage 영속)
└── store/ Zustand (auth/menu/theme)
public/
├── sw.js Service Worker — fetch 캐시 + push/notificationclick
├── manifest.json PWA 매니페스트
├── icon-{192,512}.png PWA 아이콘 (모모 로고)
├── badge-96.png 알림 상태바 단색 배지
└── .well-known/assetlinks.json TWA Digital Asset Links
android/
├── twa-manifest.json Bubblewrap TWA 빌드 설정 (notification delegation ON)
└── README.md APK/AAB 빌드 가이드 (PWABuilder / Bubblewrap)
```
각 디렉토리별 상세는 `*/CLAUDE.md` 참고.
---
## 로컬 개발
```bash
npm install
npm run dev # http://localhost:3000
npm run dev # http://localhost:3000
npm run build # 운영 빌드 검증
npm run lint
```
## 환경변수
### 환경변수 (`.env.development`)
`.env.development`의 DB 접속 정보를 확인. 필수 키:
| 키 | 설명 |
|---|---|
| `DATABASE_URL` | PostgreSQL 접속 (예: `postgresql://momo_app:****@121.156.99.3:5432/distribution`) |
| `NEXTAUTH_URL` | 로컬: `http://localhost:3000` |
| `NEXTAUTH_SECRET` | JWT 서명 시크릿 |
| `AES_KEY` | 16바이트 — 비밀번호 AES 키 (기존 데이터 호환 필요) |
| `MASTER_PWD` | 마스터 비밀번호 (개발 편의) |
| `SMTP_HOST/USER/PASS/FROM` | 거래명세표·계산서 메일 발송 |
| `MOMO_BANK_ACCOUNT`, `MOMO_PHONE` | 거래명세표 공급자 정보 (기준명세표 미설정 폴백) |
| `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` | (선택) 웹푸시 VAPID — 미설정 시 `lib/push.ts` 의 기본키 사용 |
- `DATABASE_URL` — 외부 PostgreSQL 접속
- `NEXTAUTH_SECRET` — JWT 서명 키
- `MASTER_PWD` — 마스터 비밀번호 (개발 편의용)
- `AES_KEY` — 비밀번호 AES 암호화 키 (기존 Java 호환)
`.env.momo.example` 참고.
## 배포 표준
---
- Docker Compose (dev/prod 분리) — 기존 FITO(Java) 배포환경 재사용
- Traefik 리버스 프록시 + `fito.wace.me` 도메인 (entrypoints: web, websecure / certresolver: le)
- 외부 네트워크 `toktork_server_default`
- DB는 외부 서버 공유 (`211.115.91.141:11140/fito`) — 컨테이너 내부 DB 없음
## 배포 — Gitea Actions 자동 배포
### `start.sh` 배포 스크립트 (권장)
`main` 브랜치에 push 하면 [`.gitea/workflows/deploy.yml`](.gitea/workflows/deploy.yml) 이 자동 실행되어 운영 서버에서 `git pull → docker compose build → up -d` 까지 수행.
```bash
# 첫 배포 (서버에서)
cp .env.production.example .env.production
vi .env.production # DATABASE_URL, NEXTAUTH_SECRET, AES_KEY 등 입력
- 운영 서버: `121.156.99.3` (SSH, chpark)
- Compose 파일: [`docker-compose.prod.yml`](docker-compose.prod.yml)
- 컨테이너명: `momo-erp` / 이미지: `momo-erp:latest`
- 리버스 프록시: Traefik (`traefik-net` 외부 네트워크, Let's Encrypt 자동발급)
- 호스트: `momotogether.com`, `www.momotogether.com`
- 영구 스토리지: named volume `momo_data_storage``/data_storage` (업로드 이미지)
- DB: 외부 공유 — 컨테이너 내부 DB 없음
./start.sh prod # git pull → build → 기동 → Traefik 라우팅 확인
> ⚠️ **수동 SSH 빌드 금지** — 자동 워크플로우와 충돌. 배포 검증은 `build-sha.txt` 또는 `docker logs momo-erp` 로 확인.
# 이후 배포 (git commit 후)
./start.sh prod # 자동 git pull + 재빌드
---
# 기타 운영
./start.sh logs prod # 실시간 로그
./start.sh restart prod # 재시작 (git pull 포함)
./start.sh stop prod # 중지
./start.sh status prod # 컨테이너 상태
./start.sh build prod # no-cache 재빌드
./start.sh clean prod # 전체 삭제 (확인 필요)
```
## Android 앱 (TWA)
스크립트는 start.sh 자체가 업데이트되면 새 버전으로 **자동 재실행**하므로 안전합니다.
`com.momotogether.app``momotogether.com` 을 감싸는 Trusted Web Activity.
### 로컬 개발
- **알림 위임(notification delegation) ON** — 웹 푸시가 "삼성 인터넷" 등 브라우저가 아니라 **모모유통 앱 이름·아이콘**으로 표시됨.
- **AAB**: Play 스토어 업로드용
- **APK**: 사이드로드(직접 설치) 테스트용
- 빌드 가이드: [`android/README.md`](android/README.md) (PWABuilder 또는 Bubblewrap)
- ⚠️ 기존 서명키 재사용 필수 — 키가 바뀌면 `assetlinks.json` 지문이 안 맞아 위임 깨짐.
```bash
./start.sh # docker 기반 (localhost:3643, hot reload)
npm run dev # docker 없이 Node 직접 (localhost:3000)
```
서비스워커 갱신 / 페이지 콘텐츠 변경은 **APK 재빌드 불필요** — 서버 배포만으로 앱에도 반영됨.
### 인프라 정보
---
- 컨테이너명: `plm-fito-next` (prod) / `plm-fito-next-dev` (dev)
- 도메인: `https://fito.wace.me`
- 내부 포트: 3000 (Traefik이 외부 80/443 → 3000)
- 파일 저장: 호스트 `./data_storage` (레포 상대경로) ↔ 컨테이너 `/data_storage`
- 이미지: Next.js `output: "standalone"` 기반 multi-stage build
## 코딩 컨벤션
상세 구성은 [CLAUDE.md](CLAUDE.md) 참고.
상세 규칙은 [`.claude/rules/`](.claude/rules/) 와 디렉토리별 `CLAUDE.md` 참고.
- **SQL alias 대문자 유지**: `SELECT col AS "OBJID"` (큰따옴표 필수, 없으면 PG 가 소문자 반환)
- **삭제 플래그**: `COALESCE(is_del,'N') != 'Y'`
- **objid 타입 변환**: `objid::text AS "OBJID"`
- **API 응답**: 목록 `{ RESULTLIST, TOTAL_CNT }`, 단건 `{ success, data }`, 저장 `{ success, objId? }`, 오류 `{ success:false, message }` + 적절한 status code
- **인증 가드**: 모든 API 라우트 첫 줄에 `getSession()` / `requireMomoAdmin()` / `requireMomoUser()`
- **신규 컬럼**: `ensureColumns` 패턴으로 라우트 첫 호출 시 자동 ALTER (운영 무중단)
- **푸시 알림 대상**: 일반 발송은 모든 구독자 / 거래처 전용은 `sendPush(payload, undefined, { generalOnly: true })`
---
## 라이선스
내부용. 외부 배포·재사용 금지.
-3
View File
@@ -1,3 +0,0 @@
Manifest-Version: 1.0
Class-Path:
-74
View File
@@ -1,74 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="ko" xml:lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>네이버 :: Smart Editor 2 &#8482;</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>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-175
View File
@@ -1,175 +0,0 @@
@charset "UTF-8";
/* NHN Web Standardization Team (http://html.nhndesign.com/) HHJ 090226 */
/* COMMON */
body,#smart_editor2,#smart_editor2 p,#smart_editor2 h1,#smart_editor2 h2,#smart_editor2 h3,#smart_editor2 h4,#smart_editor2 h5,#smart_editor2 h6,#smart_editor2 ul,#smart_editor2 ol,#smart_editor2 li,#smart_editor2 dl,#smart_editor2 dt,#smart_editor2 dd,#smart_editor2 table,#smart_editor2 th,#smart_editor2 td,#smart_editor2 form,#smart_editor2 fieldset,#smart_editor2 legend,#smart_editor2 input,#smart_editor2 textarea,#smart_editor2 button,#smart_editor2 select{margin:0;padding:0}
#smart_editor2,#smart_editor2 h1,#smart_editor2 h2,#smart_editor2 h3,#smart_editor2 h4,#smart_editor2 h5,#smart_editor2 h6,#smart_editor2 input,#smart_editor2 textarea,#smart_editor2 select,#smart_editor2 table,#smart_editor2 button{font-family:'돋움',Dotum,Helvetica,sans-serif;font-size:12px;color:#666}
#smart_editor2 span,#smart_editor2 em{font-size:12px}
#smart_editor2 em,#smart_editor2 address{font-style:normal}
#smart_editor2 img,#smart_editor2 fieldset{border:0}
#smart_editor2 hr{display:none}
#smart_editor2 ol,#smart_editor2 ul{list-style:none}
#smart_editor2 button{border:0;background:none;font-size:11px;vertical-align:top;cursor:pointer}
#smart_editor2 button span,#smart_editor2 button em{visibility:hidden;overflow:hidden;position:absolute;top:0;font-size:0;line-height:0}
#smart_editor2 legend,#smart_editor2 .blind{visibility:hidden;overflow:hidden;position:absolute;width:0;height:0;font-size:0;line-height:0}
#smart_editor2 .input_ty1{height:14px;margin:0;padding:4px 2px 0 4px;border:1px solid #c7c7c7;font-size:11px;color:#666}
#smart_editor2 a:link,#smart_editor2 a:visited,#smart_editor2 a:active,#smart_editor2 a:focus{color:#666;text-decoration:none}
#smart_editor2 a:hover{color:#666;text-decoration:underline}
/* LAYOUT */
#smart_editor2 .se2_header{margin:10px 0 29px 0}
#smart_editor2 .se2_bi{float:left;width:93px;height:20px;margin:0;padding:0;background:url("../img/ko_KR/btn_set.png?130306") -343px -358px no-repeat;font-size:0;line-height:0;text-indent:-10000px;vertical-align:middle}
#smart_editor2 .se2_allhelp{display:inline-block;width:18px;height:18px;padding:0;background:url("../img/ko_KR/btn_set.png?130306") -437px -358px no-repeat;font-size:0;line-height:0;text-indent:-10000px;vertical-align:middle}
#smart_editor2 #smart_editor2_content{border:1px solid #b5b5b5}
#smart_editor2 .se2_tool{overflow:visible;position:relative;z-index:25}
/* EDITINGAREA */
#smart_editor2 .se2_input_area{position:relative;z-index:22;height:400px;margin:0;padding:0;*zoom:1}
#smart_editor2 .se2_input_wysiwyg,#smart_editor2 .se2_input_syntax{display:block;overflow:auto;width:100%;height:100%;margin:0;*margin:-1px 0 0 0;border:0}
/* EDITINGMODE */
#smart_editor2 .se2_conversion_mode{position:relative;height:15px;padding-top:1px;border-top:1px solid #b5b5b5;background:url("../img/icon_set.gif") 0 -896px repeat-x}
#smart_editor2 .se2_inputarea_controller{display:block;clear:both;position:relative;width:100%;height:15px;text-align:center;cursor:n-resize}
#smart_editor2 .se2_inputarea_controller span,#smart_editor2 .controller_on span{background:url("../img/ico_extend.png") no-repeat}
#smart_editor2 .se2_inputarea_controller span{position:static;display:inline-block;visibility:visible;overflow:hidden;height:15px;padding-left:11px;background-position:0 2px;color:#888;font-size:11px;letter-spacing:-1px;line-height:16px;white-space:nowrap}
* + html #smart_editor2 .se2_inputarea_controller span{line-height:14px}
#smart_editor2 .controller_on span{background-position:0 -21px;color:#249c04}
#smart_editor2 .ly_controller{display:block;position:absolute;bottom:2px;left:50%;width:287px;margin-left:-148px;padding:8px 0 7px 9px;border:1px solid #827f7c;background:#fffdef}
#smart_editor2 .ly_controller p{color:#666;font-size:11px;letter-spacing:-1px;line-height:11px}
#smart_editor2 .ly_controller .bt_clse,#smart_editor2 .ly_controller .ic_arr{position:absolute;background:url("../img/ico_extend.png") no-repeat}
#smart_editor2 .ly_controller .bt_clse{top:5px;right:4px;width:14px;height:15px;background-position:1px -43px}
#smart_editor2 .ly_controller .ic_arr{top:25px;left:50%;width:10px;height:6px;margin-left:-5px;background-position:0 -65px}
#smart_editor2 .se2_converter{float:left;position:absolute;top:-1px;right:3px;z-index:20}
#smart_editor2 .se2_converter li{float:left}
#smart_editor2 .se2_converter .se2_to_editor{width:59px;height:15px;background:url("../img/ko_KR/btn_set.png?130306") 0 -85px no-repeat;vertical-align:top}
#smart_editor2 .se2_converter .se2_to_html{width:59px;height:15px;background:url("../img/ko_KR/btn_set.png?130306") -59px -70px no-repeat;vertical-align:top}
#smart_editor2 .se2_converter .se2_to_text{width:60px;height:15px;background:url("../img/ko_KR/btn_set.png?130306") -417px -466px no-repeat;vertical-align:top}
#smart_editor2 .se2_converter .active .se2_to_editor{width:59px;height:15px;background:url("../img/ko_KR/btn_set.png?130306") 0 -70px no-repeat;vertical-align:top}
#smart_editor2 .se2_converter .active .se2_to_html{width:59px;height:15px;background:url("../img/ko_KR/btn_set.png?130306") -59px -85px no-repeat;vertical-align:top}
#smart_editor2 .se2_converter .active .se2_to_text{width:60px;height:15px;background:url("../img/ko_KR/btn_set.png?130306") -417px -481px no-repeat;vertical-align:top}
/* EDITINGAREA_HTMLSRC */
#smart_editor2 .off .ico_btn,#smart_editor2 .off .se2_more,#smart_editor2 .off .se2_more2,#smart_editor2 .off .se2_font_family,#smart_editor2 .off .se2_font_size,#smart_editor2 .off .se2_bold,#smart_editor2 .off .se2_underline,#smart_editor2 .off .se2_italic,#smart_editor2 .off .se2_tdel,#smart_editor2 .off .se2_fcolor,#smart_editor2 .off .se2_fcolor_more,#smart_editor2 .off .se2_bgcolor,#smart_editor2 .off .se2_bgcolor_more,#smart_editor2 .off .se2_left,#smart_editor2 .off .se2_center,#smart_editor2 .off .se2_right,#smart_editor2 .off .se2_justify,#smart_editor2 .off .se2_ol,#smart_editor2 .off .se2_ul,#smart_editor2 .off .se2_indent,#smart_editor2 .off .se2_outdent,#smart_editor2 .off .se2_lineheight,#smart_editor2 .off .se2_del_style,#smart_editor2 .off .se2_blockquote,#smart_editor2 .off .se2_summary,#smart_editor2 .off .se2_footnote,#smart_editor2 .off .se2_url,#smart_editor2 .off .se2_emoticon,#smart_editor2 .off .se2_character,#smart_editor2 .off .se2_table,#smart_editor2 .off .se2_find,#smart_editor2 .off .se2_spelling,#smart_editor2 .off .se2_sup,#smart_editor2 .off .se2_sub,#smart_editor2 .off .se2_text_tool_more,#smart_editor2 .off .se2_new,#smart_editor2 .off .selected_color,#smart_editor2 .off .se2_lineSticker{-ms-filter:alpha(opacity=50);opacity:.5;cursor:default;filter:alpha(opacity=50)}
/* LAYER */
#smart_editor2 .se2_text_tool .se2_layer{display:none;float:left;position:absolute;top:20px;left:0;z-index:50;margin:0;padding:0;border:1px solid #bcbbbb;background:#fafafa}
#smart_editor2 .se2_text_tool li.active{z-index:50}
#smart_editor2 .se2_text_tool .active .se2_layer{display:block}
#smart_editor2 .se2_text_tool .active li .se2_layer{display:none}
#smart_editor2 .se2_text_tool .active .active .se2_layer{display:block}
#smart_editor2 .se2_text_tool .se2_layer .se2_in_layer{float:left;margin:0;padding:0;border:1px solid #fff;background:#fafafa}
/* TEXT_TOOLBAR */
#smart_editor2 .se2_text_tool{position:relative;clear:both;z-index:30;padding:4px 0 4px 3px;background:#f4f4f4 url("../img/bg_text_tool.gif") 0 0 repeat-x;border-bottom:1px solid #b5b5b5;*zoom:1}
#smart_editor2 .se2_text_tool:after{content:"";display:block;clear:both}
#smart_editor2 .se2_text_tool ul{float:left;display:inline;margin-right:3px;padding-left:1px;white-space:nowrap}
#smart_editor2 .se2_text_tool li{_display:inline;float:left;position:relative;z-index:30}
#smart_editor2 .se2_text_tool button,#smart_editor2 .se2_multy .se2_icon{width:21px;height:21px;background:url("../img/ko_KR/text_tool_set.png?140317") no-repeat;vertical-align:top}
#smart_editor2 .se2_text_tool .se2_font_type{position:relative}
#smart_editor2 .se2_text_tool .se2_font_type li{margin-left:3px}
#smart_editor2 .se2_text_tool .se2_font_type button{text-align:left}
#smart_editor2 .se2_text_tool .se2_font_type button.se2_font_family span,#smart_editor2 .se2_text_tool .se2_font_type button.se2_font_size span{display:inline-block;visibility:visible;position:static;width:52px;height:20px;padding:0 0 0 6px;font-size:12px;line-height:20px;*line-height:22px;color:#333;*zoom:1}
#smart_editor2 .se2_text_tool .se2_multy{position:absolute;top:0;right:0;padding-left:0;margin-right:0;white-space:nowrap;border-left:1px solid #e0dedf}
#smart_editor2 .se2_text_tool .se2_multy .se2_mn{float:left;white-space:nowrap}
#smart_editor2 .se2_text_tool .se2_multy button{background-image:none;width:47px}
#smart_editor2 .se2_text_tool .se2_multy .se2_icon{display:inline-block;visibility:visible;overflow:visible;position:static;width:16px;height:29px;margin:-1px 2px 0 -1px;background-position:0 -132px;line-height:30px;vertical-align:top}
#smart_editor2 .se2_text_tool .se2_multy button,#smart_editor2 .se2_text_tool .se2_multy button span{height:29px;line-height:29px}
#smart_editor2 .se2_text_tool .se2_map .se2_icon{background-position:-29px -132px}
#smart_editor2 .se2_text_tool button span.se2_mntxt{display:inline-block;visibility:visible;overflow:visible;_overflow-y:hidden;position:relative;*margin-right:-1px;width:auto;height:29px;font-weight:normal;font-size:11px;line-height:30px;*line-height:29px;_line-height:30px;color:#444;letter-spacing:-1px;vertical-align:top}
#smart_editor2 .se2_text_tool .se2_multy .se2_photo{margin-right:1px}
#smart_editor2 .se2_text_tool .se2_multy .hover .ico_btn{background:#e8e8e8}
#smart_editor2 .se2_text_tool .se2_multy .se2_mn.hover{background:#e0dedf}
/* TEXT_TOOLBAR : ROUNDING */
#smart_editor2 ul li.first_child button span.tool_bg,#smart_editor2 ul li.last_child button span.tool_bg,#smart_editor2 ul li.single_child button span.tool_bg{visibility:visible;height:21px}
#smart_editor2 ul li.first_child button span.tool_bg{left:-1px;width:3px;background:url("../img/bg_button_left.gif?20121228") no-repeat}
#smart_editor2 ul li.last_child button span.tool_bg{right:0px;_right:-1px;width:2px;background:url("../img/bg_button_right.gif") no-repeat}
#smart_editor2 ul li.single_child{padding-right:1px}
#smart_editor2 ul li.single_child button span.tool_bg{left:0;background:url("../img/bg_button.gif?20121228") no-repeat;width:22px}
#smart_editor2 div.se2_text_tool ul li.hover button span.tool_bg{background-position:0 -21px}
#smart_editor2 div.se2_text_tool ul li.active button span.tool_bg,#smart_editor2 div.se2_text_tool ul li.active li.active button span.tool_bg{background-position:0 -42px}
#smart_editor2 div.se2_text_tool ul li.active li button span.tool_bg{background-position:0 0}
/* TEXT_TOOLBAR : SUB_MENU */
#smart_editor2 .se2_sub_text_tool{display:none;position:absolute;top:20px;left:0;z-index:40;width:auto;height:29px;padding:0 4px 0 0;border:1px solid #b5b5b5;border-top:1px solid #9a9a9a;background:#f4f4f4}
#smart_editor2 .active .se2_sub_text_tool{display:block}
#smart_editor2 .se2_sub_text_tool ul{float:left;height:25px;margin:0;padding:4px 0 0 4px}
/* TEXT_TOOLBAR : SUB_MENU_SIZE */
#smart_editor2 .se2_sub_step1{width:88px}
#smart_editor2 .se2_sub_step2{width:199px}
#smart_editor2 .se2_sub_step2_1{width:178px}
/* TEXT_TOOLBAR : BUTTON */
#smart_editor2 .se2_text_tool .se2_font_family{width:70px;height:21px;background-position:0 -10px}
#smart_editor2 .se2_text_tool .hover .se2_font_family{background-position:0 -72px}
#smart_editor2 .se2_text_tool .active .se2_font_family{background-position:0 -103px}
#smart_editor2 .se2_text_tool .se2_font_size{width:45px;height:21px;background-position:-70px -10px}
#smart_editor2 .se2_text_tool .hover .se2_font_size{background-position:-70px -72px}
#smart_editor2 .se2_text_tool .active .se2_font_size{background-position:-70px -103px}
#smart_editor2 .se2_text_tool .se2_bold{background-position:-115px -10px}
#smart_editor2 .se2_text_tool .hover .se2_bold{background-position:-115px -72px}
#smart_editor2 .se2_text_tool .active .se2_bold{background-position:-115px -103px}
#smart_editor2 .se2_text_tool .se2_underline{background-position:-136px -10px}
#smart_editor2 .se2_text_tool .hover .se2_underline{background-position:-136px -72px}
#smart_editor2 .se2_text_tool .active .se2_underline{background-position:-136px -103px}
#smart_editor2 .se2_text_tool .se2_italic{background-position:-157px -10px}
#smart_editor2 .se2_text_tool .hover .se2_italic{background-position:-157px -72px}
#smart_editor2 .se2_text_tool .active .se2_italic{background-position:-157px -103px}
#smart_editor2 .se2_text_tool .se2_tdel{background-position:-178px -10px}
#smart_editor2 .se2_text_tool .hover .se2_tdel{background-position:-178px -72px}
#smart_editor2 .se2_text_tool .active .se2_tdel{background-position:-178px -103px}
#smart_editor2 .se2_text_tool .se2_fcolor{position:relative;background-position:-199px -10px}
#smart_editor2 .se2_text_tool .hover .se2_fcolor{background-position:-199px -72px}
#smart_editor2 .se2_text_tool .active .se2_fcolor{background-position:-199px -103px}
#smart_editor2 .se2_text_tool .se2_fcolor_more{background-position:-220px -10px;width:10px}
#smart_editor2 .se2_text_tool .hover .se2_fcolor_more{background-position:-220px -72px}
#smart_editor2 .se2_text_tool .active .se2_fcolor_more{background-position:-220px -103px}
#smart_editor2 .se2_text_tool .selected_color{position:absolute;top:14px;left:5px;width:11px;height:3px;font-size:0}
#smart_editor2 .se2_text_tool .se2_ol,#smart_editor2 .se2_text_tool .active .se2_sub_text_tool .se2_ol{background-position:-345px -10px}
#smart_editor2 .se2_text_tool .se2_ul,#smart_editor2 .se2_text_tool .active .se2_sub_text_tool .se2_ul{background-position:-366px -10px}
#smart_editor2 .se2_text_tool .hover .se2_ol,#smart_editor2 .se2_text_tool .active .se2_sub_text_tool .hover .se2_ol{background-position:-345px -72px}
#smart_editor2 .se2_text_tool .hover .se2_ul,#smart_editor2 .se2_text_tool .active .se2_sub_text_tool .hover .se2_ul{background-position:-366px -72px}
#smart_editor2 .se2_text_tool .active .se2_ol,#smart_editor2 .se2_text_tool .active .active .se2_ol{background-position:-345px -103px}
#smart_editor2 .se2_text_tool .active .se2_ul,#smart_editor2 .se2_text_tool .active .active .se2_ul{background-position:-366px -103px}
#smart_editor2 .se2_text_tool .se2_indent,#smart_editor2 .se2_text_tool .active .se2_sub_text_tool .se2_indent{background-position:-408px -10px}
#smart_editor2 .se2_text_tool .se2_outdent,#smart_editor2 .se2_text_tool .active .se2_sub_text_tool .se2_outdent{background-position:-387px -10px}
#smart_editor2 .se2_text_tool .hover .se2_indent,#smart_editor2 .se2_text_tool .active .se2_sub_text_tool .hover .se2_indent{background-position:-408px -72px}
#smart_editor2 .se2_text_tool .hover .se2_outdent,#smart_editor2 .se2_text_tool .active .se2_sub_text_tool .hover .se2_outdent{background-position:-387px -72px}
#smart_editor2 .se2_text_tool .active .se2_indent,#smart_editor2 .se2_text_tool .active .active .se2_indent{background-position:-408px -103px}
#smart_editor2 .se2_text_tool .active .se2_outdent,#smart_editor2 .se2_text_tool .active .active .se2_outdent{background-position:-387px -103px}
#smart_editor2 .se2_text_tool .se2_lineheight{background-position:-429px -10px}
#smart_editor2 .se2_text_tool .hover .se2_lineheight{background-position:-429px -72px}
#smart_editor2 .se2_text_tool .active .se2_lineheight{background-position:-429px -103px}
#smart_editor2 .se2_text_tool .se2_url{background-position:-513px -10px}
#smart_editor2 .se2_text_tool .hover .se2_url{background-position:-513px -72px}
#smart_editor2 .se2_text_tool .active .se2_url{background-position:-513px -103px}
#smart_editor2 .se2_text_tool .se2_bgcolor{position:relative;background-position:-230px -10px}
#smart_editor2 .se2_text_tool .hover .se2_bgcolor{background-position:-230px -72px}
#smart_editor2 .se2_text_tool .active .se2_bgcolor{background-position:-230px -103px}
#smart_editor2 .se2_text_tool .se2_bgcolor_more{background-position:-251px -10px;width:10px}
#smart_editor2 .se2_text_tool .hover .se2_bgcolor_more{background-position:-251px -72px}
#smart_editor2 .se2_text_tool .active .se2_bgcolor_more{background-position:-251px -103px}
#smart_editor2 .se2_text_tool .se2_left{background-position:-261px -10px}
#smart_editor2 .se2_text_tool .hover .se2_left{background-position:-261px -72px}
#smart_editor2 .se2_text_tool .active .se2_left{background-position:-261px -103px}
#smart_editor2 .se2_text_tool .se2_center{background-position:-282px -10px}
#smart_editor2 .se2_text_tool .hover .se2_center{background-position:-282px -72px}
#smart_editor2 .se2_text_tool .active .se2_center{background-position:-282px -103px}
#smart_editor2 .se2_text_tool .se2_right{background-position:-303px -10px}
#smart_editor2 .se2_text_tool .hover .se2_right{background-position:-303px -72px}
#smart_editor2 .se2_text_tool .active .se2_right{background-position:-303px -103px}
#smart_editor2 .se2_text_tool .se2_justify{background-position:-324px -10px}
#smart_editor2 .se2_text_tool .hover .se2_justify{background-position:-324px -72px}
#smart_editor2 .se2_text_tool .active .se2_justify{background-position:-324px -103px}
#smart_editor2 .se2_text_tool .se2_blockquote{background-position:-471px -10px}
#smart_editor2 .se2_text_tool .hover .se2_blockquote{background-position:-471px -72px}
#smart_editor2 .se2_text_tool .active .se2_blockquote{background-position:-471px -103px}
#smart_editor2 .se2_text_tool .se2_character{background-position:-555px -10px}
#smart_editor2 .se2_text_tool .hover .se2_character{background-position:-555px -72px}
#smart_editor2 .se2_text_tool .active .se2_character{background-position:-555px -103px}
#smart_editor2 .se2_text_tool .se2_table{background-position:-576px -10px}
#smart_editor2 .se2_text_tool .hover .se2_table{background-position:-576px -72px}
#smart_editor2 .se2_text_tool .active .se2_table{background-position:-576px -103px}
#smart_editor2 .se2_text_tool .se2_find{background-position:-597px -10px}
#smart_editor2 .se2_text_tool .hover .se2_find{background-position:-597px -72px}
#smart_editor2 .se2_text_tool .active .se2_find{background-position:-597px -103px}
#smart_editor2 .se2_text_tool .se2_sup{background-position:-660px -10px}
#smart_editor2 .se2_text_tool .hover .se2_sup{background-position:-660px -72px}
#smart_editor2 .se2_text_tool .active .se2_sup{background-position:-660px -103px}
#smart_editor2 .se2_text_tool .se2_sub{background-position:-681px -10px}
#smart_editor2 .se2_text_tool .hover .se2_sub{background-position:-681px -72px}
#smart_editor2 .se2_text_tool .active .se2_sub{background-position:-681px -103px}
#smart_editor2 .se2_text_tool .se2_text_tool_more{background-position:0 -41px;width:13px}
#smart_editor2 .se2_text_tool .se2_text_tool_more span.tool_bg{background:none}
#smart_editor2 .se2_text_tool .hover .se2_text_tool_more{background-position:-13px -41px}
#smart_editor2 .se2_text_tool .active .se2_text_tool_more{background-position:-26px -41px}
-21
View File
@@ -1,21 +0,0 @@
@charset "UTF-8";
/* NHN Web Standardization Team (http://html.nhndesign.com/) HHJ 090226 */
/* COMMON */
body,.se2_inputarea{margin:0;padding:0;font-family:'돋움',Dotum,Helvetica,Sans-serif;font-size:12px;line-height:1.5}
/* body,.se2_inputarea,.se2_inputarea th,.se2_inputarea td{margin:0;padding:0;font-family:'돋움',Dotum,Helvetica,Sans-serif;font-size:12px;line-height:1.5;color:#666} */
.se2_inputarea p,.se2_inputarea br{margin:0;padding:0}
.se2_inputarea{margin:15px;word-wrap:break-word;*word-wrap:normal;*word-break:break-all}
.se2_inputarea td{word-break:break-all}
.se2_inputarea_890{width:741px;margin:20px 0 10px 64px}
.se2_inputarea_698{width:548px;margin:20px 0 10px 64px}
/* TEXT_TOOLBAR : QUOTE */
.se2_quote1{margin:0 0 30px 20px;padding:0 8px;border-left:2px solid #ccc;color:#888}
.se2_quote2{margin:0 0 30px 13px;padding:0 8px 0 16px;background:url("../img/bg_quote2.gif") 0 3px no-repeat;color:#888}
.se2_quote3{margin:0 0 30px;padding:12px 10px 11px;border:1px dashed #ccc;color:#888}
.se2_quote4{margin:0 0 30px;padding:12px 10px 11px;border:1px dashed #66b246;color:#888}
.se2_quote5{margin:0 0 30px;padding:12px 10px 11px;border:1px dashed #ccc;background:#fafafa;color:#888}
.se2_quote6{margin:0 0 30px;padding:12px 10px 11px;border:1px solid #e5e5e5;color:#888}
.se2_quote7{margin:0 0 30px;padding:12px 10px 11px;border:1px solid #66b246;color:#888}
.se2_quote8{margin:0 0 30px;padding:12px 10px 11px;border:1px solid #e5e5e5;background:#fafafa;color:#888}
.se2_quote9{margin:0 0 30px;padding:12px 10px 11px;border:2px solid #e5e5e5;color:#888}
.se2_quote10{margin:0 0 30px;padding:12px 10px 11px;border:2px solid #e5e5e5;background:#fafafa;color:#888}
-462
View File
@@ -1,462 +0,0 @@
@charset "UTF-8";
/* NHN Web Standardization Team (http://html.nhndesign.com/) HHJ 090226 */
/* TEXT_TOOLBAR : FONTNAME */
#smart_editor2 .se2_tool .se2_l_font_fam{width:202px;margin:0;padding:0}
#smart_editor2 .se2_tool .se2_l_font_fam li{display:block;width:202px;height:21px;margin:0;padding:0;color:#333;cursor:pointer}
#smart_editor2 .se2_l_font_fam .hover,#smart_editor2 .se2_l_font_fam .active{background:#ebebeb}
#smart_editor2 .se2_l_font_fam button{width:200px;height:21px;margin:0;padding:2px 0 2px 0px;background:none;text-align:left}
#smart_editor2 .se2_l_font_fam button span{display:block;visibility:visible;overflow:visible;position:relative;top:auto;left:auto;width:auto;height:auto;margin:0 0 0 4px;padding:0;font-size:12px;line-height:normal;color:#333}
#smart_editor2 .se2_l_font_fam button span span{display:inline;visibility:visible;overflow:visible;width:auto;height:auto;margin:0 0 0 4px;font-family:Verdana;font-size:12px;line-height:14px;color:#888}
#smart_editor2 .se2_l_font_fam button span em{visibility:visible;overflow:auto;position:static;width:auto;height:auto;margin-right:-4px;font-size:12px;color:#888}
#smart_editor2 .se2_l_font_fam .se2_division{width:162px;height:2px !important;margin:1px 0 1px 0px;border:0;background:url("../img/bg_line1.gif") 0 0 repeat-x;font-size:0;cursor:default}
/* TEXT_TOOLBAR : FONTSIZE */
#smart_editor2 .se2_tool .se2_l_font_size{width:302px;margin:0;padding:0}
#smart_editor2 .se2_tool .se2_l_font_size li{width:302px;margin:0;padding:0;color:#333;cursor:pointer}
#smart_editor2 .se2_l_font_size .hover,#smart_editor2 .se2_l_font_size .active{background:#ebebeb}
#smart_editor2 .se2_l_font_size button{width:300px;height:auto;margin:0;padding:2px 0 1px 0px;*padding:4px 0 1px 0px;background:none;text-align:left}
#smart_editor2 .se2_l_font_size button span{display:block;visibility:visible;overflow:visible;position:relative;top:auto;left:auto;width:auto;height:auto;margin:0 0 0 4px;padding:0;line-height:normal;color:#373737;letter-spacing:0px}
#smart_editor2 .se2_l_font_size button span span{display:inline;margin:0 0 0 5px;padding:0}
#smart_editor2 .se2_l_font_size span em{visibility:visible;overflow:auto;position:static;width:auto;height:auto;color:#888}
/* TEXT_TOOLBAR : FONTCOLOR */
#smart_editor2 .se2_palette{float:left;position:relative;width:225px;margin:0;padding:11px 0 10px 0}
#smart_editor2 .se2_palette .se2_pick_color{_display:inline;float:left;clear:both;width:205px;margin:0 0 0 11px;padding:0}
#smart_editor2 .se2_palette .se2_pick_color li{float:left;width:12px;height:12px;margin:0;padding:0}
#smart_editor2 .se2_palette .se2_pick_color li button{width:11px;height:11px;border:0}
#smart_editor2 .se2_palette .se2_pick_color li button span{display:block;visibility:visible;overflow:visible;position:absolute;top:1px;left:1px;width:11px;height:11px}
#smart_editor2 .se2_palette .se2_pick_color li button span span{visibility:hidden;overflow:hidden;position:absolute;top:0;left:0;width:0;height:0}
#smart_editor2 .se2_palette .se2_pick_color .hover button,#smart_editor2 .se2_palette .se2_pick_color .active button{width:11px;height:11px;border:1px solid #666}
#smart_editor2 .se2_palette .se2_pick_color .hover span,#smart_editor2 .se2_palette .se2_pick_color .active span{width:7px;height:7px;border:1px solid #fff}
#smart_editor2 .se2_palette .se2_view_more{_display:inline;float:left;width:46px;height:23px;margin:1px 0 0 1px;background:url("../img/ko_KR/btn_set.png?130306") 0 -47px no-repeat}
#smart_editor2 .se2_palette .se2_view_more2{_display:inline;float:left;width:46px;height:23px;margin:1px 0 0 1px;background:url("../img/ko_KR/btn_set.png?130306") 0 -24px no-repeat}
#smart_editor2 .se2_palette h4{_display:inline;float:left;width:203px;margin:9px 0 0 11px;padding:10px 0 4px 0;background:url("../img/bg_line1.gif") repeat-x;font-weight:normal;font-size:12px;line-height:14px;color:#333;letter-spacing:-1px}
#smart_editor2 .se2_palette2{float:left;_float:none;width:214px;margin:9px 0 0 0;padding:11px 0 0 11px;background:url("../img/bg_line1.gif") repeat-x}
#smart_editor2 .se2_palette2 .se2_color_set{float:left}
#smart_editor2 .se2_palette2 .se2_selected_color{_display:inline;float:left;width:83px;height:18px;margin:0;border:1px solid #c7c7c7;background:#fff}
#smart_editor2 .se2_palette2 .se2_selected_color span{_display:inline;float:left;width:79px;height:14px;margin:2px}
#smart_editor2 .se2_palette2 .input_ty1{_display:inline;float:left;width:67px;height:16px;margin:0 3px 0 3px;padding:2px 2px 0 4px;font-family:tahoma;font-size:11px}
#smart_editor2 .se2_palette2 button.se2_btn_insert{float:left;width:35px;height:21px;margin-left:2px;padding:0;background:url("../img/ko_KR/btn_set.png?130306") -80px 0 no-repeat}
#smart_editor2 .se2_gradation1{float:left;_float:none;width:201px;height:128px;margin:4px 0 0 0;border:1px solid #c7c7c7;cursor:crosshair}
#smart_editor2 .se2_gradation2{float:left;_float:none;width:201px;height:10px;margin:4px 0 1px 0;border:1px solid #c7c7c7;cursor:crosshair}
/* TEXT_TOOLBAR : BGCOLOR */
#smart_editor2 .se2_palette_bgcolor{width:225px;margin:11px 0 0;padding:0}
#smart_editor2 .se2_palette_bgcolor .se2_background{width:205px;margin:0 11px 0 11px}
#smart_editor2 .se2_palette_bgcolor .se2_background li{width:68px;height:20px}
#smart_editor2 .se2_palette_bgcolor .se2_background button{width:67px;height:19px;border:0}
#smart_editor2 .se2_palette_bgcolor .se2_background span{left:0;display:block;visibility:visible;overflow:visible;width:65px;height:17px;padding:0}
#smart_editor2 .se2_palette_bgcolor .se2_background span span{display:block;visibility:visible;overflow:visible;width:64px;height:16px;padding:3px 0 0 3px;font-size:11px;line-height:14px;text-align:left}
#smart_editor2 .se2_palette_bgcolor .se2_background .hover span{width:65px;height:17px;border:1px solid #666}
#smart_editor2 .se2_palette_bgcolor .se2_background .hover span span{width:62px;height:14px;padding:1px 0 0 1px;border:1px solid #fff}
/* TEXT_TOOLBAR : LINEHEIGHT */
#smart_editor2 .se2_l_line_height{width:107px;margin:0;padding:0}
#smart_editor2 .se2_l_line_height li{width:107px;margin:0;padding:0;border-top:0;border-bottom:0;color:#333;cursor:pointer}
#smart_editor2 .se2_l_line_height .hover{background:#ebebeb}
#smart_editor2 .se2_l_line_height button{width:105px;height:19px;margin:0;padding:3px 0 2px 0px;background:none;text-align:left}
#smart_editor2 .se2_l_line_height button span{visibility:visible;overflow:visible;position:relative;width:auto;height:auto;margin:0;padding:0 0 0 15px;font-size:12px;line-height:normal;color:#373737}
#smart_editor2 .se2_l_line_height li button.active span{background:url("../img/icon_set.gif") 5px -30px no-repeat}
#smart_editor2 .se2_l_line_height_user{clear:both;width:83px;margin:5px 0 0 12px;padding:10px 0 0 0;_padding:11px 0 0 0;background:url("../img/bg_line1.gif") repeat-x}
#smart_editor2 .se2_l_line_height_user h3{margin:0 0 4px 0;_margin:0 0 2px -1px;padding:0;line-height:14px;color:#000;letter-spacing:-1px}
#smart_editor2 .se2_l_line_height_user .bx_input{display:block;position:relative;width:83px}
#smart_editor2 .se2_l_line_height_user .btn_up{position:absolute;top:2px;*top:3px;left:68px;width:13px;height:8px;background:url("../img/ko_KR/btn_set.png?130306") -86px -54px no-repeat}
#smart_editor2 .se2_l_line_height_user .btn_down{position:absolute;top:10px;*top:11px;left:68px;width:13px;height:8px;background:url("../img/ko_KR/btn_set.png?130306") -86px -62px no-repeat}
#smart_editor2 .se2_l_line_height_user .btn_area{margin:5px 0 10px 0}
#smart_editor2 .se2_tool .btn_area .se2_btn_apply3{width:41px;height:24px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat}
#smart_editor2 .se2_tool .btn_area .se2_btn_cancel3{width:39px;height:24px;margin-left:3px;background:url("../img/ko_KR/btn_set.png?130306") -41px 0 no-repeat}
/* TEXT_TOOLBAR : QUOTE */
#smart_editor2 .se2_quote{width:425px;height:56px}
#smart_editor2 .se2_quote ul{_display:inline;float:left;margin:11px 0 0 9px;padding:0}
#smart_editor2 .se2_quote li{_display:inline;float:left;margin:0 0 0 2px;padding:0}
#smart_editor2 .se2_quote button{width:34px;height:34px;margin:0;padding:0;background:url("../img/ko_KR/btn_set.png?130306") no-repeat;cursor:pointer}
#smart_editor2 .se2_quote button span{left:0;display:block;visibility:visible;overflow:visible;width:32px;height:32px;margin:0;padding:0;border:1px solid #c7c7c7}
#smart_editor2 .se2_quote button span span{visibility:hidden;overflow:hidden;position:absolute;top:0;left:0;width:0;height:0;margin:0;padding:0}
#smart_editor2 .se2_quote .se2_quote1{background-position:1px -375px}
#smart_editor2 .se2_quote .se2_quote2{background-position:-32px -375px}
#smart_editor2 .se2_quote .se2_quote3{background-position:-65px -375px}
#smart_editor2 .se2_quote .se2_quote4{background-position:-98px -375px}
#smart_editor2 .se2_quote .se2_quote5{background-position:-131px -375px}
#smart_editor2 .se2_quote .se2_quote6{background-position:-164px -375px}
#smart_editor2 .se2_quote .se2_quote7{background-position:-197px -375px}
#smart_editor2 .se2_quote .se2_quote8{background-position:-230px -375px}
#smart_editor2 .se2_quote .se2_quote9{background-position:-263px -375px}
#smart_editor2 .se2_quote .se2_quote10{background-position:-296px -375px}
#smart_editor2 .se2_quote .hover button span,#smart_editor2 .se2_quote .active button span{width:30px;height:30px;margin:0;padding:0;border:2px solid #44b525}
#smart_editor2 .se2_quote .hover button span span,#smart_editor2 .se2_quote .active button span span{visibility:hidden;overflow:hidden;position:absolute;top:0;left:0;width:0;height:0;margin:0;padding:0}
#smart_editor2 .se2_quote .se2_cancel2{float:left;width:40px;height:35px;margin:11px 0 0 5px;background:url("../img/ko_KR/btn_set.png?130306") -46px -24px no-repeat}
#smart_editor2 .se2_quote .se2_cancel2 span{visibility:hidden;overflow:hidden;position:absolute;top:0;left:0;width:0;height:0;margin:0;padding:0}
/* TEXT_TOOLBAR : HYPERLINK */
#smart_editor2 .se2_url2{width:281px;padding:11px 11px 6px 11px;color:#666}
#smart_editor2 .se2_url2 .input_ty1{display:block;width:185px;height:16px;margin:0 5px 5px 0;*margin:-1px 5px 5px 0;padding:5px 2px 0 4px}
#smart_editor2 .se2_url2 .se2_url_new{width:15px;height:15px;margin:-1px 3px 1px -1px;*margin:-2px 3px 2px -1px;vertical-align:middle}
#smart_editor2 .se2_url2 label{font-size:11px;line-height:14px;vertical-align:middle}
#smart_editor2 .se2_url2 .se2_apply{position:absolute;top:13px;right:51px;width:41px;height:24px;margin:-1px 3px 1px 0;background:url("../img/ko_KR/btn_set.png?130306") no-repeat}
#smart_editor2 .se2_url2 .se2_cancel{position:absolute;top:13px;right:9px;width:39px;height:24px;margin:-1px 3px 1px 0;background:url("../img/ko_KR/btn_set.png?130306") -41px 0 no-repeat}
/* TEXT_TOOLBAR : SCHARACTER */
#smart_editor2 .se2_bx_character{width:469px;height:272px;margin:0;padding:0;background:url("../img/ko_KR/bx_set_110302.gif") 9px -1230px no-repeat}
#smart_editor2 .se2_bx_character .se2_char_tab{_display:inline;float:left;position:relative;width:443px;margin:11px 10px 200px 11px;padding:0 0 0 1px}
#smart_editor2 .se2_bx_character .se2_char_tab li{position:static;margin:0 0 0 -1px;padding:0}
#smart_editor2 .se2_bx_character .se2_char1{width:76px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") 0 -204px no-repeat}
#smart_editor2 .se2_bx_character .se2_char2{width:86px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -75px -204px no-repeat}
#smart_editor2 .se2_bx_character .se2_char3{width:68px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -160px -204px no-repeat}
#smart_editor2 .se2_bx_character .se2_char4{width:55px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -227px -204px no-repeat}
#smart_editor2 .se2_bx_character .se2_char5{width:97px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -281px -204px no-repeat}
#smart_editor2 .se2_bx_character .se2_char6{width:66px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -377px -204px no-repeat}
#smart_editor2 .se2_bx_character .active .se2_char1{width:76px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") 0 -230px no-repeat}
#smart_editor2 .se2_bx_character .active .se2_char2{width:86px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -75px -230px no-repeat}
#smart_editor2 .se2_bx_character .active .se2_char3{width:68px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -160px -230px no-repeat}
#smart_editor2 .se2_bx_character .active .se2_char4{width:55px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -227px -230px no-repeat}
#smart_editor2 .se2_bx_character .active .se2_char5{width:97px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -281px -230px no-repeat}
#smart_editor2 .se2_bx_character .active .se2_char6{width:66px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -377px -230px no-repeat}
#smart_editor2 .se2_bx_character .se2_s_character{display:none;position:absolute;top:26px;left:0;width:448px;height:194px;margin:0;padding:0}
#smart_editor2 .se2_bx_character .active .se2_s_character{display:block}
#smart_editor2 .se2_bx_character .se2_s_character ul{float:left;width:422px;height:172px;margin:0;padding:9px 0 0 11px}
#smart_editor2 .se2_bx_character .se2_s_character li{_display:inline;float:left;position:relative;width:20px;height:18px;margin:0 0 1px 1px;background:#fff}
#smart_editor2 .se2_bx_character .se2_s_character button{width:20px;height:18px;margin:0;padding:2px;background:none}
#smart_editor2 .se2_bx_character .se2_s_character .hover,#smart_editor2 .se2_bx_character .se2_s_character .active{background:url("../img/ko_KR/btn_set.png?130306") -446px -274px no-repeat}
#smart_editor2 .se2_bx_character .se2_s_character button span{left:0;display:block;visibility:visible;overflow:visible;width:14px;height:16px;margin:3px 0 0 3px;border:0;background:none;font-size:12px;line-height:normal}
#smart_editor2 .se2_apply_character{clear:both;position:relative;padding:0 0 0 11px}
#smart_editor2 .se2_apply_character label{margin:0 3px 0 0;font-size:12px;color:#666;letter-spacing:-1px}
#smart_editor2 .se2_apply_character .input_ty1{width:283px;height:17px;margin:-1px 5px 1px 0;padding:4px 0 0 5px;font-size:12px;color:#666;letter-spacing:0;vertical-align:middle}
#smart_editor2 .se2_apply_character .se2_confirm{width:41px;height:24px;margin-right:3px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat;vertical-align:middle}
#smart_editor2 .se2_apply_character .se2_cancel{width:39px;height:24px;background:url("../img/ko_KR/btn_set.png?130306") -41px 0 no-repeat;vertical-align:middle}
/* TEXT_TOOLBAR : TABLECREATOR */
#smart_editor2 .se2_table_set{position:relative;width:166px;margin:3px 11px 0 11px;padding:8px 0 0 0}
#smart_editor2 .se2_table_set .se2_cell_num{float:left;width:73px}
#smart_editor2 .se2_table_set .se2_cell_num dt{float:left;clear:both;width:17px;height:23px;margin:0;padding:0}
#smart_editor2 .se2_table_set .se2_cell_num dt label{display:block;margin:5px 0 0 0;font-size:11px;color:#666}
#smart_editor2 .se2_table_set .se2_cell_num dd{float:left;position:relative;width:54px;height:23px;margin:0;padding:0}
#smart_editor2 .se2_table_set .se2_cell_num .input_ty2{display:block;width:32px;height:16px;*margin:-1px 0 0 0;padding:2px 19px 0 0px;border:1px solid #c7c7c7;font-family:tahoma,verdana,times New Roman;font-size:11px;color:#666;text-align:right;*direction:rtl}
#smart_editor2 .se2_table_set .se2_cell_num .input_ty2::-ms-clear{display:none}
#smart_editor2 .se2_table_set .se2_pre_table{float:right;width:91px;height:43px;background:#c7c7c7;border-spacing:1px}
#smart_editor2 .se2_table_set .se2_pre_table tr{background:#fff}
#smart_editor2 .se2_table_set .se2_pre_table td{font-size:0;line-height:0}
#smart_editor2 .se2_table_set .se2_add{position:absolute;top:2px;right:3px;width:13px;height:8px;background:url("../img/ko_KR/btn_set.png?130306") -86px -54px no-repeat}
#smart_editor2 .se2_table_set .se2_del{position:absolute;top:10px;right:3px;width:13px;height:8px;background:url("../img/ko_KR/btn_set.png?130306") -86px -62px no-repeat}
/* TEXT_TOOLBAR : TABLEEDITOR */
#smart_editor2 .se2_table_set .se2_t_proper1{float:left;width:166px;margin:7px 0 0 0;padding:10px 0 5px;background:url("../img/bg_line1.gif") repeat-x}
#smart_editor2 .se2_table_set .se2_t_proper1 dt{width:166px;margin:0 0 6px 0}
#smart_editor2 .se2_table_set .se2_t_proper1 dd{width:166px}
#smart_editor2 .se2_table_set .se2_t_proper1 dt input{width:15px;height:15px;margin:-1px 3px 1px 0;_margin:-2px 3px 2px 0;vertical-align:middle}
#smart_editor2 .se2_table_set .se2_t_proper1 dt label{font-weight:bold;font-size:11px;color:#666;letter-spacing:-1px;vertical-align:middle}
#smart_editor2 .se2_table_set .se2_t_proper1_1{float:left;position:relative;z-index:59;width:166px;margin:1px 0 0 0}
#smart_editor2 .se2_table_set .se2_t_proper1_2{z-index:54;margin:0}
#smart_editor2 .se2_table_set .se2_t_proper1_3{z-index:53;margin:0}
#smart_editor2 .se2_table_set .se2_t_proper1_4{z-index:52;margin:0}
#smart_editor2 .se2_table_set .se2_t_proper1_1 dt{_display:inline;float:left;clear:both;width:66px;height:22px;margin:1px 0 0 18px}
#smart_editor2 .se2_table_set .se2_t_proper1_1 dt label{display:block;margin:4px 0 0 0;font-weight:normal;font-size:11px;color:#666;letter-spacing:-1px}
#smart_editor2 .se2_table_set .se2_t_proper1_1 dd{float:left;position:relative;width:82px;height:23px}
#smart_editor2 .se2_table_set .se2_t_proper1_1 .input_ty1{width:72px;height:16px;*margin:-1px 0 0 0;padding:2px 2px 0 6px;font-family:tahoma,verdana,times New Roman;font-size:11px;color:#666}
#smart_editor2 .se2_table_set .se2_t_proper1_1 .input_ty3{float:left;width:49px;height:16px;margin:0 3px 0 0;padding:2px 4px 0 4px;border:1px solid #c7c7c7;font-family:tahoma,verdana,times New Roman;font-size:11px;color:#666}
#smart_editor2 .se2_table_set .se2_t_proper1_1 .se2_add{top:2px;right:2px}
#smart_editor2 .se2_table_set .se2_t_proper1_1 .se2_del{top:10px;right:2px}
#smart_editor2 .se2_table_set .se2_t_proper1_1 .se2_color_set .input_ty1{_display:inline;float:left;width:67px;height:16px;margin:0 3px 0 3px;padding:2px 2px 0 4px;font-family:tahoma,verdana,times New Roman;font-size:11px}
#smart_editor2 .se2_select_ty1{position:relative;width:80px;height:18px;border:1px solid #c7c7c7;background:#fff;font-size:11px;line-height:14px;text-align:left}
#smart_editor2 .se2_select_ty1 span{float:left;width:54px;height:18px;margin:0 0 0 5px;font-size:11px;line-height:14px;color:#666}
#smart_editor2 .se2_select_ty1 .se2_b_style0{position:relative;top:3px;left:-3px;white-space:nowrap}
#smart_editor2 .se2_select_ty1 .se2_b_style1{height:15px;margin:3px 0 0 4px;font-size:11px;line-height:14px;color:#666;letter-spacing:-1px}
#smart_editor2 .se2_select_ty1 .se2_b_style2{background:url("../img/bg_set.gif") 0 -50px repeat-x}
#smart_editor2 .se2_select_ty1 .se2_b_style3{background:url("../img/bg_set.gif") 0 -68px repeat-x}
#smart_editor2 .se2_select_ty1 .se2_b_style4{background:url("../img/bg_set.gif") 0 -85px repeat-x}
#smart_editor2 .se2_select_ty1 .se2_b_style5{background:url("../img/bg_set.gif") 0 -103px repeat-x}
#smart_editor2 .se2_select_ty1 .se2_b_style6{background:url("../img/bg_set.gif") 0 -121px repeat-x}
#smart_editor2 .se2_select_ty1 .se2_b_style7{background:url("../img/bg_set.gif") 0 -139px repeat-x}
#smart_editor2 .se2_select_ty1 .se2_view_more{position:absolute;top:1px;right:1px;width:13px;height:16px;background:url("../img/ko_KR/btn_set.png?130306") -112px -54px no-repeat}
#smart_editor2 .se2_select_ty1 .se2_view_more2{position:absolute;top:1px;right:1px;width:13px;height:16px;background:url("../img/ko_KR/btn_set.png?130306") -99px -54px no-repeat}
/* TEXT_TOOLBAR : TABLEEDITOR > BORDER */
#smart_editor2 .se2_table_set .se2_b_t_b1{border-top:1px solid #b1b1b1}
#smart_editor2 .se2_layer_b_style{position:absolute;top:20px;right:0px;width:80px;padding-bottom:1px;border:1px solid #c7c7c7;border-top:1px solid #a8a8a8;background:#fff}
#smart_editor2 .se2_layer_b_style ul{width:80px;margin:0;padding:1px 0 0 0}
#smart_editor2 .se2_layer_b_style li{width:80px;height:18px;margin:0;padding:0}
#smart_editor2 .se2_layer_b_style .hover,#smart_editor2 .se2_layer_b_style .active{background:#ebebeb}
#smart_editor2 .se2_layer_b_style button{width:80px;height:18px;background:none}
#smart_editor2 .se2_layer_b_style button span{left:0;display:block;visibility:visible;overflow:visible;width:71px;height:18px;margin:0 0 0 5px;font-size:11px;line-height:15px;text-align:left}
#smart_editor2 .se2_layer_b_style button span span{visibility:hidden;overflow:hidden;position:absolute;top:0;left:0;width:0;height:0}
#smart_editor2 .se2_layer_b_style .se2_b_style1 span{margin:3px 0 0 4px;font-size:11px;line-height:14px;color:#666;letter-spacing:-1px}
#smart_editor2 .se2_layer_b_style .se2_b_style2 span{background:url("../img/bg_set.gif") 0 -50px repeat-x}
#smart_editor2 .se2_layer_b_style .se2_b_style3 span{background:url("../img/bg_set.gif") 0 -68px repeat-x}
#smart_editor2 .se2_layer_b_style .se2_b_style4 span{background:url("../img/bg_set.gif") 0 -86px repeat-x}
#smart_editor2 .se2_layer_b_style .se2_b_style5 span{background:url("../img/bg_set.gif") 0 -103px repeat-x}
#smart_editor2 .se2_layer_b_style .se2_b_style6 span{background:url("../img/bg_set.gif") 0 -121px repeat-x}
#smart_editor2 .se2_layer_b_style .se2_b_style7 span{background:url("../img/bg_set.gif") 0 -139px repeat-x}
/* TEXT_TOOLBAR : TABLEEDITOR > COLOR */
#smart_editor2 .se2_pre_color{float:left;width:18px;height:18px;border:1px solid #c7c7c7}
#smart_editor2 .se2_pre_color button{float:left;width:14px;height:14px;margin:2px 0 0 2px;padding:0}
#smart_editor2 .se2_pre_color button span{overflow:hidden;position:absolute;top:-10000px;left:-10000px;z-index:-100;width:0;height:0}
/* TEXT_TOOLBAR : TABLEEDITOR > DIMMED */
#smart_editor2 .se2_table_set .se2_t_dim1{clear:both;position:absolute;top:71px;left:16px;z-index:60;width:157px;height:118px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_table_set .se2_t_dim2{position:absolute;top:116px;left:16px;z-index:55;width:157px;height:45px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_table_set .se2_t_dim3{clear:both;position:absolute;top:192px;left:16px;z-index:51;width:157px;height:39px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
/* TEXT_TOOLBAR : TABLEEDITOR > STYLE PREVIEW */
#smart_editor2 .se2_table_set .se2_t_proper2{float:left;position:relative;z-index:50;width:166px;margin:2px 0 0 0}
#smart_editor2 .se2_table_set .se2_t_proper2 dt{float:left;width:84px;height:33px;margin:4px 0 0 0}
#smart_editor2 .se2_table_set .se2_t_proper2 dt input{width:15px;height:15px;margin:-1px 3px 1px 0;_margin:-2px 3px 2px 0;vertical-align:middle}
#smart_editor2 .se2_table_set .se2_t_proper2 dt label{font-weight:bold;font-size:11px;color:#666;letter-spacing:-1px;vertical-align:middle}
#smart_editor2 .se2_table_set .se2_t_proper2 dd{float:left;width:66px;height:33px}
#smart_editor2 .se2_select_ty2{position:relative;width:65px;height:31px;border:1px solid #c7c7c7;background:#fff;font-size:11px;line-height:14px;text-align:left}
#smart_editor2 .se2_select_ty2 span{float:left;width:45px;height:25px;margin:3px 0 0 3px;background:url("../img/ko_KR/btn_set.png?130306") repeat-x}
#smart_editor2 .se2_select_ty2 .se2_t_style1{background-position:0 -410px}
#smart_editor2 .se2_select_ty2 .se2_t_style2{background-position:-46px -410px}
#smart_editor2 .se2_select_ty2 .se2_t_style3{background-position:-92px -410px}
#smart_editor2 .se2_select_ty2 .se2_t_style4{background-position:-138px -410px}
#smart_editor2 .se2_select_ty2 .se2_t_style5{background-position:-184px -410px}
#smart_editor2 .se2_select_ty2 .se2_t_style6{background-position:-230px -410px}
#smart_editor2 .se2_select_ty2 .se2_t_style7{background-position:-276px -410px}
#smart_editor2 .se2_select_ty2 .se2_t_style8{background-position:-322px -410px}
#smart_editor2 .se2_select_ty2 .se2_t_style9{background-position:0 -436px}
#smart_editor2 .se2_select_ty2 .se2_t_style10{background-position:-46px -436px}
#smart_editor2 .se2_select_ty2 .se2_t_style11{background-position:-92px -436px}
#smart_editor2 .se2_select_ty2 .se2_t_style12{background-position:-138px -436px}
#smart_editor2 .se2_select_ty2 .se2_t_style13{background-position:-184px -436px}
#smart_editor2 .se2_select_ty2 .se2_t_style14{background-position:-230px -436px}
#smart_editor2 .se2_select_ty2 .se2_t_style15{background-position:-276px -436px}
#smart_editor2 .se2_select_ty2 .se2_t_style16{background-position:-322px -436px}
#smart_editor2 .se2_select_ty2 .se2_view_more{position:absolute;top:1px;right:1px;_right:0px;width:13px !important;height:29px !important;background:url("../img/ko_KR/btn_set.png?130306") -353px -48px no-repeat !important}
#smart_editor2 .se2_select_ty2 .se2_view_more2{position:absolute;top:1px;right:1px;_right:0px;width:13px !important;height:29px !important;background:url("../img/ko_KR/btn_set.png?130306") -340px -48px no-repeat !important}
#smart_editor2 .se2_select_ty2 .se2_view_more span{display:none}
/* TEXT_TOOLBAR : TABLEEDITOR > STYLE */
#smart_editor2 .se2_layer_t_style{position:absolute;top:33px;right:15px;width:208px;border:1px solid #c7c7c7;border-top:1px solid #a8a8a8;background:#fff}
#smart_editor2 .se2_layer_t_style ul{width:204px;height:126px;margin:1px 2px;padding:1px 0 0 0;background:#fff}
#smart_editor2 .se2_layer_t_style li{_display:inline;float:left;width:45px;height:25px;margin:1px;padding:1px;border:1px solid #fff}
#smart_editor2 .se2_layer_t_style .hover,#smart_editor2 .se2_layer_t_style .active{border:1px solid #666;background:#fff}
#smart_editor2 .se2_layer_t_style button{width:45px;height:25px;background:url("../img/ko_KR/btn_set.png?130306") repeat-x !important}
#smart_editor2 .se2_layer_t_style .se2_t_style1{background-position:0 -410px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style2{background-position:-46px -410px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style3{background-position:-92px -410px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style4{background-position:-138px -410px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style5{background-position:-184px -410px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style6{background-position:-230px -410px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style7{background-position:-276px -410px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style8{background-position:-322px -410px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style9{background-position:0 -436px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style10{background-position:-46px -436px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style11{background-position:-92px -436px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style12{background-position:-138px -436px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style13{background-position:-184px -436px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style14{background-position:-230px -436px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style15{background-position:-276px -436px !important}
#smart_editor2 .se2_layer_t_style .se2_t_style16{background-position:-322px -436px !important}
#smart_editor2 .se2_table_set .se2_btn_area{float:left;width:166px;margin:6px 0 0 0;padding:12px 0 8px 0;background:url("../img/bg_line1.gif") repeat-x;text-align:center}
#smart_editor2 .se2_table_set button.se2_apply{width:41px;height:24px;margin-right:3px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat}
#smart_editor2 .se2_table_set button.se2_cancel{width:39px;height:24px;background:url("../img/ko_KR/btn_set.png?130306") -41px 0 no-repeat}
#smart_editor2 .se2_table_set .se2_rd{width:14px;height:14px;vertical-align:middle}
#smart_editor2 .se2_table_set .se2_celltit{font-size:11px;font-size:11px;color:#666;letter-spacing:-1px}
#smart_editor2 .se2_table_set dt label.se2_celltit{display:inline}
/* TEXT_TOOLBAR : FINDREPLACE */
#smart_editor2 .se2_bx_find_revise{position:relative;width:255px;margin:0;padding:0}
#smart_editor2 .se2_bx_find_revise .se2_close{position:absolute;top:5px;right:8px;width:20px;height:20px;background:url("../img/ko_KR/btn_set.png?130306") -151px -1px no-repeat}
#smart_editor2 .se2_bx_find_revise h3{margin:0;padding:10px 0 13px 10px;background:url("../img/bg_find_h3.gif") 0 -1px repeat-x;font-size:12px;line-height:14px;letter-spacing:-1px}
#smart_editor2 .se2_bx_find_revise ul{position:relative;margin:8px 0 0 0;padding:0 0 0 12px}
#smart_editor2 .se2_bx_find_revise ul li{_display:inline;float:left;position:static;margin:0 0 0 -1px;padding:0}
#smart_editor2 .se2_bx_find_revise .se2_tabfind{width:117px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") 0 -100px no-repeat}
#smart_editor2 .se2_bx_find_revise .se2_tabrevise{width:117px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -116px -100px no-repeat}
#smart_editor2 .se2_bx_find_revise .active .se2_tabfind{width:117px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") 0 -126px no-repeat}
#smart_editor2 .se2_bx_find_revise .active .se2_tabrevise{width:117px;height:26px;background:url("../img/ko_KR/btn_set.png?130306") -116px -126px no-repeat}
#smart_editor2 .se2_bx_find_revise .se2_in_bx_find dl{_display:inline;float:left;width:223px;margin:0 0 0 9px;padding:7px 0 13px 14px;background:url("../img/ko_KR/bx_set_110302.gif") -289px -1518px no-repeat}
#smart_editor2 .se2_bx_find_revise .se2_in_bx_revise dl{_display:inline;float:left;width:223px;margin:0 0 0 9px;padding:7px 0 13px 14px;background:url("../img/ko_KR/bx_set_110302.gif") -289px -1619px no-repeat}
#smart_editor2 .se2_bx_find_revise dt{_display:inline;float:left;clear:both;width:47px;margin:1px 0 2px 0}
#smart_editor2 .se2_bx_find_revise dd{float:left;margin:0 0 2px 0}
#smart_editor2 .se2_bx_find_revise label{float:left;padding:5px 0 0 0;font-size:11px;color:#666;letter-spacing:-2px}
#smart_editor2 .se2_bx_find_revise input{float:left;width:155px;height:12px;margin:1px 0 0 0;padding:3px 2px 3px 4px;font-size:12px;color:#666}
#smart_editor2 .se2_bx_find_revise .se2_find_btns{float:left;clear:both;width:255px;padding:8px 0 10px 0;text-align:center}
#smart_editor2 .se2_bx_find_revise .se2_find_next{width:65px;height:24px;margin:0 3px 0 0;background:url("../img/ko_KR/btn_set.png?130306") -180px -48px no-repeat}
#smart_editor2 .se2_bx_find_revise .se2_find_next2{width:61px;height:24px;margin:0 3px 0 0;background:url("../img/ko_KR/btn_set.png?130306") -180px -24px no-repeat}
#smart_editor2 .se2_bx_find_revise .se2_revise1{width:54px;height:24px;margin:0 3px 0 0;background:url("../img/ko_KR/btn_set.png?130306") -245px -48px no-repeat}
#smart_editor2 .se2_bx_find_revise .se2_revise2{width:70px;height:24px;margin:0 3px 0 0;background:url("../img/ko_KR/btn_set.png?130306") -245px -24px no-repeat}
#smart_editor2 .se2_bx_find_revise .se2_cancel{width:39px;height:24px;background:url("../img/ko_KR/btn_set.png?130306") -41px 0 no-repeat}
/* TEXT_TOOLBAR : QUICKEDITOR_TABLE */
#smart_editor2 .se2_qmax{position:absolute;width:18px;height:18px;background:url("../img/ko_KR/btn_set.png?130306") -339px -169px no-repeat}
#smart_editor2 .se2_qeditor{position:absolute;top:0;left:0;width:183px;margin:0;padding:0;border:1px solid #c7c7c7;border-right:1px solid #ababab;border-bottom:1px solid #ababab;background:#fafafa}
#smart_editor2 .se2_qeditor label,#smart_editor2 .se2_qeditor span,#smart_editor2 .se2_qeditor dt{font-size:11px;color:#666;letter-spacing:-1px}
#smart_editor2 .se2_qbar{position:relative;width:183px;height:11px;background:url("../img/ko_KR/bx_set_110302.gif") 0 -731px no-repeat}
#smart_editor2 .se2_qbar .se2_qmini{position:absolute;top:-1px;right:0;*right:-1px;_right:-3px;width:18px;height:14px;background:url("../img/ko_KR/btn_set.png?130306") -315px -170px no-repeat}
#smart_editor2 .se2_qbar .se2_qmini button{width:20px;height:14px;margin-top:-1px}
#smart_editor2 .se2_qeditor .se2_qbody0{float:left;border:1px solid #fefefe}
#smart_editor2 .se2_qeditor .se2_qbody{position:relative;z-index:90;width:174px;padding:4px 0 0 7px}
#smart_editor2 .se2_qeditor .se2_qe1{overflow:hidden;width:174px}
#smart_editor2 .se2_qeditor .se2_qe1 dt{float:left;width:22px;height:18px;padding:4px 0 0 0}
#smart_editor2 .se2_qeditor .se2_qe1 dd{float:left;width:65px;height:22px}
#smart_editor2 .se2_qeditor .se2_addrow{width:28px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -385px -49px}
#smart_editor2 .se2_qeditor .se2_addcol{width:29px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -413px -49px}
#smart_editor2 .se2_qeditor .se2_seprow{width:28px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -385px -68px}
#smart_editor2 .se2_qeditor .se2_sepcol{width:29px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -413px -68px}
#smart_editor2 .se2_qeditor .se2_delrow{width:28px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -385px -106px}
#smart_editor2 .se2_qeditor .se2_delcol{width:29px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -413px -106px}
#smart_editor2 .se2_qeditor .se2_merrow{width:57px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -385px -125px}
#smart_editor2 .se2_qeditor .se2_mercol{width:57px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -413px -125px}
#smart_editor2 .se2_qeditor .se2_seprow_off{width:28px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -385px -87px}
#smart_editor2 .se2_qeditor .se2_sepcol_off{width:29px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -413px -87px}
#smart_editor2 .se2_qeditor .se2_merrow_off{width:57px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -385px -144px}
#smart_editor2 .se2_qeditor .se2_mercol_off{width:57px;height:19px;background:url("../img/ko_KR/btn_set.png?130306") no-repeat -413px -144px}
/* TEXT_TOOLBAR : QUICKEDITOR_TABLE > CELL_BACKGROUND */
#smart_editor2 .se2_qeditor .se2_qe2{_display:inline;float:left;position:relative;z-index:100;width:165px;margin:2px 0 0 1px;padding:7px 0 0 0;background:url("../img/bg_line1.gif") repeat-x;zoom:1}
#smart_editor2 .se2_qeditor .se2_qe2_1 dt{float:left;width:62px;padding:3px 0 0 0}
#smart_editor2 .se2_qeditor .se2_qe2_1 dt input{width:15px;height:15px;margin:-1px 1px 1px -1px;vertical-align:middle}
#smart_editor2 .se2_qeditor .se2_qe2_1 dd{float:left;position:relative;zoom:1}
#smart_editor2 .se2_qeditor .se2_qe2_3{padding:7px 0 6px 0}
/* My글양식 없을때 */
#smart_editor2 .se2_qeditor .se2_qe2_2{position:relative;_position:absolute}
#smart_editor2 .se2_qeditor .se2_qe2_2 dt{float:left;width:50px;padding:3px 0 0 13px}
#smart_editor2 .se2_qeditor .se2_qe2_2 dt input{width:15px;height:15px;margin:-1px 2px 1px -1px;vertical-align:middle}
#smart_editor2 .se2_qeditor .se2_qe2_2 dd{float:left}
/* TEXT_TOOLBAR : QUICKEDITOR_TABLE > STYLE */
#smart_editor2 .se2_table_set .se2_qbody .se2_t_proper2{float:left;*float:none;position:static;width:166px;margin:5px 0 0 1px}
#smart_editor2 .se2_qeditor .se2_qe3 dt{float:left;width:62px;padding:0}
#smart_editor2 .se2_qeditor .se2_qe3 dt label{font-weight:normal}
#smart_editor2 .se2_qeditor .se2_qe3 dt input{width:15px;height:15px;margin:-1px 1px 1px -1px;vertical-align:middle}
#smart_editor2 .se2_qeditor .se2_qe3 dd .se2_qe3_table{position:relative}
/* TEXT_TOOLBAR : QUICKEDITOR_TABLE > CELL_BACKGROUND PREWVIEW */
#smart_editor2 .se2_qeditor .se2_pre_color{float:left;width:18px;height:18px;border:1px solid #c7c7c7}
#smart_editor2 .se2_qeditor .se2_pre_color button{float:left;width:14px;height:14px;margin:2px 0 0 2px;padding:0}
#smart_editor2 .se2_qeditor .se2_pre_color button span{overflow:hidden;position:absolute;top:-10000px;left:-10000px;z-index:-100;width:0;height:0}
/* TEXT_TOOLBAR : QUICKEDITOR_TABLE > CELL_BACKGROUND LAYER */
#smart_editor2 .se2_qeditor .se2_layer{float:left;clear:both;position:absolute;top:20px;left:0;margin:0;padding:0;border:1px solid #c7c7c7;border-top:1px solid #9a9a9a;background:#fafafa}
#smart_editor2 .se2_qeditor .se2_layer .se2_in_layer{float:left;margin:0;padding:0;border:1px solid #fff;background:#fafafa}
#smart_editor2 .se2_qeditor .se2_layer button{vertical-align:top}
#smart_editor2 .se2_qeditor .se2_layer .se2_pick_color li{position:relative}
/* TEXT_TOOLBAR : QUICKEDITOR_TABLE > CELL_BACKGROUND IMAGE */
#smart_editor2 .se2_qeditor .se2_pre_bgimg{float:left;width:14px;height:14px;padding:2px;border:1px solid #c7c7c7}
#smart_editor2 .se2_qeditor .se2_qe2_2 button{width:16px;height:16px;background:url("../img/ko_KR/btn_set.png?130306") 0 -261px no-repeat}
/* TEXT_TOOLBAR : QUICKEDITOR_TABLE > CELL_BACKGROUND IMAGE LAYER */
#smart_editor2 .se2_cellimg_set{_display:inline;float:left;width:136px;margin:4px 3px 0 4px;padding-bottom:4px}
#smart_editor2 .se2_cellimg_set li{_display:inline;float:left;width:16px;height:16px;margin:0 1px 1px 0}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg0{background-position:-255px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg1{background-position:0 -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg2{background-position:-17px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg3{background-position:-34px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg4{background-position:-51px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg5{background-position:-68px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg6{background-position:-85px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg7{background-position:-102px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg8{background-position:-119px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg9{background-position:-136px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg10{background-position:-153px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg11{background-position:-170px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg12{background-position:-187px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg13{background-position:-204px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg14{background-position:-221px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg15{background-position:-238px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg16{background-position:-255px -261px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg17{background-position:0 -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg18{background-position:-17px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg19{background-position:-34px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg20{background-position:-51px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg21{background-position:-68px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg22{background-position:-85px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg23{background-position:-102px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg24{background-position:-119px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg25{background-position:-136px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg26{background-position:-153px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg27{background-position:-170px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg28{background-position:-187px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg29{background-position:-204px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg30{background-position:-221px -278px}
#smart_editor2 .se2_qeditor .se2_qe2_2 .se2_cellimg31{background-position:-238px -278px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg button{width:14px;height:14px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg1{background-position:-1px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg2{background-position:-18px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg3{background-position:-35px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg4{background-position:-52px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg5{background-position:-69px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg6{background-position:-86px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg7{background-position:-103px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg8{background-position:-120px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg9{background-position:-137px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg10{background-position:-154px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg11{background-position:-171px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg12{background-position:-188px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg13{background-position:-205px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg14{background-position:-222px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg15{background-position:-239px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg16{background-position:-256px -262px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg17{background-position:-1px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg18{background-position:-18px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg19{background-position:-35px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg20{background-position:-52px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg21{background-position:-69px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg22{background-position:-86px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg23{background-position:-103px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg24{background-position:-120px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg25{background-position:-137px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg26{background-position:-154px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg27{background-position:-171px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg28{background-position:-188px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg29{background-position:-205px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg30{background-position:-222px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg31{background-position:-239px -279px}
#smart_editor2 .se2_qeditor .se2_pre_bgimg .se2_cellimg32{background-position:-256px -279px}
/* TEXT_TOOLBAR : QUICKEDITOR_TABLE > MY REVIEW */
#smart_editor2 .se2_btn_area{_display:inline;float:left;clear:both;width:166px;margin:5px 0 0 1px;padding:7px 0 6px 0;background:url("../img/bg_line1.gif") repeat-x;text-align:center}
#smart_editor2 .se2_btn_area .se2_btn_save{width:97px;height:21px;background:url("../img/ko_KR/btn_set.png?130306") -369px -163px no-repeat}
/* TEXT_TOOLBAR : QUICKEDITOR_IMAGE */
#smart_editor2 .se2_qe10{width:166px;margin:0;*margin:-2px 0 0 0}
#smart_editor2 .se2_qe10 label{margin:0 1px 0 0;vertical-align:middle}
#smart_editor2 .se2_qe10 .se2_sheight{margin-left:4px}
#smart_editor2 .se2_qe10 .input_ty1{width:30px;height:13px;margin:0 0 1px 1px;padding:3px 4px 0 1px;font-size:11px;letter-spacing:0;text-align:right;vertical-align:middle}
#smart_editor2 .se2_qe10 .se2_sreset{width:41px;height:19px;margin-left:3px;background:url("../img/ko_KR/btn_set.png?130306") -401px -184px no-repeat;vertical-align:middle}
#smart_editor2 .se2_qe10_1{margin-top:4px;padding:10px 0 3px;background:url("../img/bg_line1.gif") repeat-x}
#smart_editor2 .se2_qe10_1 input{width:15px;height:15px;margin:-1px 3px 1px -1px;vertical-align:middle}
#smart_editor2 .se2_qe11{float:left;width:166px;margin:4px 0 0 0;padding:7px 0 2px 0;background:url("../img/bg_line1.gif") repeat-x}
#smart_editor2 .se2_qe11_1{float:left;width:99px}
#smart_editor2 .se2_qe11_1 dt{float:left;width:56px;height:15px;padding:5px 0 0 0}
#smart_editor2 .se2_qe11_1 dd{float:left;position:relative;width:38px;height:20px}
#smart_editor2 .se2_qe11_1 .input_ty1{display:block;width:29px;height:15px;margin:0;*margin:-1px 0 1px 0;padding:3px 1px 0 5px;font-size:11px;letter-spacing:0;text-align:left}
#smart_editor2 .se2_qe11_1 .se2_add{position:absolute;top:2px;right:3px;width:13px;height:8px;background:url("../img/ko_KR/btn_set.png?130306") -86px -54px no-repeat}
#smart_editor2 .se2_qe11_1 .se2_del{position:absolute;top:10px;right:3px;width:13px;height:8px;background:url("../img/ko_KR/btn_set.png?130306") -86px -62px no-repeat}
#smart_editor2 .se2_qe11_2{float:left;width:67px}
#smart_editor2 .se2_qe11_2 dt{float:left;width:47px;margin:5px 0 0 0}
#smart_editor2 .se2_qe11_2 dd{float:left;position:relative;width:20px}
#smart_editor2 .se2_qe12{float:left;width:166px;margin:3px 0 0 0;padding:7px 0 0 0;background:url("../img/bg_line1.gif") repeat-x}
#smart_editor2 .se2_qe12 dt{float:left;margin:5px 4px 0 0}
#smart_editor2 .se2_qe12 dd{float:left;padding:0 0 6px 0}
#smart_editor2 .se2_qe12 .se2_align0{float:left;width:19px;height:21px;background:url("../img/ko_KR/btn_set.png?130306") -276px -121px no-repeat}
#smart_editor2 .se2_qe12 .se2_align1{float:left;width:19px;height:21px;background:url("../img/ko_KR/btn_set.png?130306") -295px -121px no-repeat}
#smart_editor2 .se2_qe12 .se2_align2{float:left;width:20px;height:21px;background:url("../img/ko_KR/btn_set.png?130306") -314px -121px no-repeat}
#smart_editor2 .se2_qe13{position:relative;z-index:10;zoom:1}
#smart_editor2 .se2_qe13 dt{float:left;width:62px;padding:3px 0 0}
#smart_editor2 .se2_qe13 dt input{width:15px;height:15px;margin:-1px 1px 1px -1px;vertical-align:middle;zoom:1}
#smart_editor2 .se2_qe13 dt .se2_qdim2{width:32px}
#smart_editor2 .se2_qe13 dd .se2_select_ty1{width:38px}
#smart_editor2 .se2_qe13 dd .se2_select_ty1 span{width:15px}
#smart_editor2 .se2_qe13 dd .input_ty1{width:20px}
#smart_editor2 .se2_qe13 dd .se2_palette2 .input_ty1{width:67px}
#smart_editor2 .se2_qe13 .se2_add{*top:3px}
#smart_editor2 .se2_qe13 .se2_del{*top:11px}
#smart_editor2 .se2_qe13 .se2_layer_b_style{right:-2px;_right:0}
#smart_editor2 .se2_qe13 .se2_layer_b_style li span{width:auto;margin:0 4px 0 5px;padding-top:2px}
#smart_editor2 .se2_qe13 dd{_display:inline;float:left;position:relative;width:29px;margin-right:5px;_margin-right:3px;zoom:1}
#smart_editor2 .se2_qe13 dd .se2_palette h4{margin-top:9px;font-family:dotum;font-size:12px}
#smart_editor2 .se2_qe13 dd.dd_type{width:38px}
#smart_editor2 .se2_qe13 dd.dd_type2{width:37px;margin-right:3px}
#smart_editor2 .se2_qe13 dd.dd_type2 .input_ty1{width:29px}
#smart_editor2 .se2_qe13 dd.dd_type2 button{right:2px;_right:1px}
#smart_editor2 .se2_qe13 dd.dd_type3{width:20px;margin:0}
#smart_editor2 .se2_qe13_v1{_display:inline;float:left;margin:2px 0 1px}
#smart_editor2 .se2_qe13_v1 dt{padding:4px 0 0 1px}
#smart_editor2 .se2_qe13_v2{_display:inline;float:left;position:relative;z-index:100;width:165px;margin:4px 0 0 1px;zoom:1}
#smart_editor2 .se2_qe13_v2 dd{width:18px;margin:0}
#smart_editor2 .se2_qeditor .se2_qdim1{clear:both;position:absolute;top:25px;left:115px;width:60px;height:23px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim2{clear:both;position:absolute;top:55px;left:24px;z-index:110;width:70px;height:22px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim3{clear:both;position:absolute;top:55px;left:118px;z-index:110;width:56px;height:22px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim4{clear:both;position:absolute;top:81px;left:23px;z-index:35;width:116px;height:35px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim5{clear:both;position:absolute;top:31px;left:106px;width:68px;height:26px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim6c{clear:both;position:absolute;top:25px;left:28px;width:29px;height:23px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim6r{clear:both;position:absolute;top:25px;left:57px;width:29px;height:23px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_highedit{float:right;width:56px;height:21px;margin:-27px 8px 0 0;background:url("../img/ko_KR/btn_set.png?130306") -329px -142px no-repeat}
#smart_editor2 .se2_qeditor .se2_qdim7{clear:both;position:absolute;top:55px;left:24px;z-index:110;width:150px;height:48px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim8{clear:both;position:absolute;top:105px;left:24px;z-index:110;width:150px;height:37px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim9{clear:both;position:absolute;top:55px;left:111px;z-index:110;width:65px;height:24px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim10{clear:both;position:absolute;top:55px;left:100px;z-index:110;width:77px;height:24px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
#smart_editor2 .se2_qeditor .se2_qdim11{clear:both;position:absolute;top:55px;left:65px;z-index:110;width:115px;height:24px;background:#fafafa;opacity:0.5;filter:alpha(opacity=50)}
/* HELP : ACCESSIBILITY */
#smart_editor2 .se2_accessibility{z-index:90}
#smart_editor2 .se2_accessibility .se2_in_layer{width:568px;padding:0 10px;background:#fafafa;border:1px solid #bcbbbb}
#smart_editor2 .se2_accessibility h3{margin:0 -10px;padding:6px 0 12px 0;background:url("../img/bg_find_h3.gif") repeat-x;font-size:12px;line-height:14px;letter-spacing:-1px}
#smart_editor2 .se2_accessibility h3 strong{display:inline-block;padding:4px 0 3px 11px;color:#333;letter-spacing:0}
#smart_editor2 .se2_accessibility .se2_close{position:absolute;top:10px;right:12px;width:13px;height:12px;background:url("../img/ko_KR/btn_set.png?130306") -155px -5px no-repeat}
#smart_editor2 .se2_accessibility .box_help{padding:0 2px;margin-top:8px;background:url("../img/bg_help.gif") 0 100% no-repeat}
#smart_editor2 .se2_accessibility .box_help div{overflow:hidden;padding:20px 21px 24px;border-top:1px solid #d0d0d0;color:#333}
#smart_editor2 .se2_accessibility .box_help strong{display:block;margin-bottom:2px}
#smart_editor2 .se2_accessibility .box_help p{margin-bottom:28px;line-height:1.5}
#smart_editor2 .se2_accessibility .box_help ul{width:150%;margin-top:10px}
#smart_editor2 .se2_accessibility .box_help li{position:relative;float:left;width:252px;padding:5px 0 5px 9px;margin-right:40px;background:url("../img/ko_KR/btn_set.png?130306") -475px -51px no-repeat;border-right:1px solid #f0f0f0;*zoom:1;line-height:1}
#smart_editor2 .se2_accessibility .box_help li span{position:absolute;top:4px;left:138px;line-height:1.2}
#smart_editor2 .se2_accessibility .se2_btns{padding:9px 0 10px;text-align:center}
#smart_editor2 .se2_accessibility .se2_btns .se2_close2{width:39px;height:24px;background:url("../img/ko_KR/btn_set.png?130306") -235px -120px no-repeat}
-12
View File
@@ -1,12 +0,0 @@
@charset "UTF-8";
/* NHN Web Standardization Team (http://html.nhndesign.com/) HHJ 090226 */
/* COMMON */
.se2_outputarea,.se2_outputarea th,.se2_outputarea td{margin:0;padding:0;color:#666;font-size:12px;font-family:'돋움',Dotum,'굴림',Gulim,Helvetica,Sans-serif;line-height:1.5}
.se2_outputarea p{margin:0;padding:0}
.se2_outputarea a:hover{text-decoration:underline}
.se2_outputarea a:link{color:#0000ff}
.se2_outputarea ul{margin:0 0 0 40px;padding:0}
.se2_outputarea ul li{margin:0;list-style-type:disc;padding:0}
.se2_outputarea ul ul li{list-style-type:circle}
.se2_outputarea ul ul ul li{list-style-type:square}
.se2_outputarea img,.se2_outputarea fieldset{border:0}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 526 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

-134
View File
@@ -1,134 +0,0 @@
if(typeof window.nhn=='undefined') window.nhn = {};
if (!nhn.husky) nhn.husky = {};
/**
* @fileOverview This file contains application creation helper function, which would load up an HTML(Skin) file and then execute a specified create function.
* @name HuskyEZCreator.js
*/
nhn.husky.EZCreator = new (function(){
this.nBlockerCount = 0;
this.createInIFrame = function(htOptions){
if(arguments.length == 1){
var oAppRef = htOptions.oAppRef;
var elPlaceHolder = htOptions.elPlaceHolder;
var sSkinURI = htOptions.sSkinURI;
var fCreator = htOptions.fCreator;
var fOnAppLoad = htOptions.fOnAppLoad;
var bUseBlocker = htOptions.bUseBlocker;
var htParams = htOptions.htParams || null;
}else{
// for backward compatibility only
var oAppRef = arguments[0];
var elPlaceHolder = arguments[1];
var sSkinURI = arguments[2];
var fCreator = arguments[3];
var fOnAppLoad = arguments[4];
var bUseBlocker = arguments[5];
var htParams = arguments[6];
}
if(bUseBlocker) nhn.husky.EZCreator.showBlocker();
var attachEvent = function(elNode, sEvent, fHandler){
if(elNode.addEventListener){
elNode.addEventListener(sEvent, fHandler, false);
}else{
elNode.attachEvent("on"+sEvent, fHandler);
}
}
if(!elPlaceHolder){
alert("Placeholder is required!");
return;
}
if(typeof(elPlaceHolder) != "object")
elPlaceHolder = document.getElementById(elPlaceHolder);
var elIFrame, nEditorWidth, nEditorHeight;
try{
elIFrame = document.createElement("<IFRAME frameborder=0 scrolling=no>");
}catch(e){
elIFrame = document.createElement("IFRAME");
elIFrame.setAttribute("frameborder", "0");
elIFrame.setAttribute("scrolling", "no");
}
elIFrame.style.width = "1px";
elIFrame.style.height = "1px";
elPlaceHolder.parentNode.insertBefore(elIFrame, elPlaceHolder.nextSibling);
attachEvent(elIFrame, "load", function(){
fCreator = elIFrame.contentWindow[fCreator] || elIFrame.contentWindow.createSEditor2;
// top.document.title = ((new Date())-window.STime);
// window.STime = new Date();
try{
nEditorWidth = elIFrame.contentWindow.document.body.scrollWidth || "500px";
nEditorHeight = elIFrame.contentWindow.document.body.scrollHeight + 12;
elIFrame.style.width = "100%";
elIFrame.style.height = nEditorHeight+ "px";
elIFrame.contentWindow.document.body.style.margin = "0";
}catch(e){
nhn.husky.EZCreator.hideBlocker(true);
elIFrame.style.border = "5px solid red";
elIFrame.style.width = "500px";
elIFrame.style.height = "500px";
alert("Failed to access "+sSkinURI);
return;
}
var oApp = fCreator(elPlaceHolder, htParams); // oEditor
oApp.elPlaceHolder = elPlaceHolder;
oAppRef[oAppRef.length] = oApp;
if(!oAppRef.getById) oAppRef.getById = {};
if(elPlaceHolder.id) oAppRef.getById[elPlaceHolder.id] = oApp;
oApp.run({fnOnAppReady:fOnAppLoad});
// top.document.title += ", "+((new Date())-window.STime);
nhn.husky.EZCreator.hideBlocker();
});
// window.STime = new Date();
elIFrame.src = sSkinURI;
this.elIFrame = elIFrame;
};
this.showBlocker = function(){
if(this.nBlockerCount<1){
var elBlocker = document.createElement("DIV");
elBlocker.style.position = "absolute";
elBlocker.style.top = 0;
elBlocker.style.left = 0;
elBlocker.style.backgroundColor = "#FFFFFF";
elBlocker.style.width = "100%";
document.body.appendChild(elBlocker);
nhn.husky.EZCreator.elBlocker = elBlocker;
}
nhn.husky.EZCreator.elBlocker.style.height = Math.max(document.body.scrollHeight, document.body.clientHeight)+"px";
this.nBlockerCount++;
};
this.hideBlocker = function(bForce){
if(!bForce){
if(--this.nBlockerCount > 0) return;
}
this.nBlockerCount = 0;
if(nhn.husky.EZCreator.elBlocker) nhn.husky.EZCreator.elBlocker.style.display = "none";
}
})();
-93
View File
@@ -1,93 +0,0 @@
function createSEditor2(elIRField, htParams, elSeAppContainer){
if(!window.$Jindo){
parent.document.body.innerHTML="진도 프레임웍이 필요합니다.<br>\n<a href='http://dev.naver.com/projects/jindo/download'>http://dev.naver.com/projects/jindo/download</a>에서 Jindo 1.5.3 버전의 jindo.min.js를 다운로드 받아 /js 폴더에 복사 해 주세요.\n(아직 Jindo 2 는 지원하지 않습니다.)";
return;
}
var elAppContainer = (elSeAppContainer || jindo.$("smart_editor2"));
var elEditingArea = jindo.$$.getSingle("DIV.husky_seditor_editing_area_container", elAppContainer);
var oWYSIWYGIFrame = jindo.$$.getSingle("IFRAME.se2_input_wysiwyg", elEditingArea);
var oIRTextarea = elIRField?elIRField:jindo.$$.getSingle("TEXTAREA.blind", elEditingArea);
var oHTMLSrc = jindo.$$.getSingle("TEXTAREA.se2_input_htmlsrc", elEditingArea);
var oTextArea = jindo.$$.getSingle("TEXTAREA.se2_input_text", elEditingArea);
if(!htParams){
htParams = {};
htParams.fOnBeforeUnload = null;
}
htParams.elAppContainer = elAppContainer; // 에디터 UI 최상위 element 셋팅
htParams.oNavigator = jindo.$Agent().navigator(); // navigator 객체 셋팅
var oEditor = new nhn.husky.HuskyCore(htParams);
oEditor.registerPlugin(new nhn.husky.CorePlugin(htParams?htParams.fOnAppLoad:null));
oEditor.registerPlugin(new nhn.husky.StringConverterManager());
var htDimension = {
nMinHeight:205,
nMinWidth:parseInt(elIRField.style.minWidth, 10)||570,
nHeight:elIRField.style.height||elIRField.offsetHeight,
nWidth:elIRField.style.width||elIRField.offsetWidth
};
var htConversionMode = {
bUseVerticalResizer : htParams.bUseVerticalResizer,
bUseModeChanger : htParams.bUseModeChanger
};
var aAdditionalFontList = htParams.aAdditionalFontList;
oEditor.registerPlugin(new nhn.husky.SE_EditingAreaManager("WYSIWYG", oIRTextarea, htDimension, htParams.fOnBeforeUnload, elAppContainer));
oEditor.registerPlugin(new nhn.husky.SE_EditingArea_WYSIWYG(oWYSIWYGIFrame)); // Tab Editor 모드
oEditor.registerPlugin(new nhn.husky.SE_EditingArea_HTMLSrc(oHTMLSrc)); // Tab HTML 모드
oEditor.registerPlugin(new nhn.husky.SE_EditingArea_TEXT(oTextArea)); // Tab Text 모드
oEditor.registerPlugin(new nhn.husky.SE2M_EditingModeChanger(elAppContainer, htConversionMode)); // 모드간 변경(Editor, HTML, Text)
oEditor.registerPlugin(new nhn.husky.SE_PasteHandler()); // WYSIWYG Paste Handler
oEditor.registerPlugin(new nhn.husky.HuskyRangeManager(oWYSIWYGIFrame));
oEditor.registerPlugin(new nhn.husky.Utils());
oEditor.registerPlugin(new nhn.husky.SE2M_UtilPlugin());
oEditor.registerPlugin(new nhn.husky.SE_WYSIWYGStyler());
oEditor.registerPlugin(new nhn.husky.SE2M_Toolbar(elAppContainer));
oEditor.registerPlugin(new nhn.husky.Hotkey()); // 단축키
oEditor.registerPlugin(new nhn.husky.SE_EditingAreaVerticalResizer(elAppContainer, htConversionMode)); // 편집영역 리사이즈
oEditor.registerPlugin(new nhn.husky.DialogLayerManager());
oEditor.registerPlugin(new nhn.husky.ActiveLayerManager());
oEditor.registerPlugin(new nhn.husky.SE_WYSIWYGStyleGetter()); // 커서 위치 스타일 정보 가져오기
oEditor.registerPlugin(new nhn.husky.SE_WYSIWYGEnterKey("P")); // 엔터 시 처리, 현재는 P로 처리
oEditor.registerPlugin(new nhn.husky.SE2M_ColorPalette(elAppContainer)); // 색상 팔레트
oEditor.registerPlugin(new nhn.husky.SE2M_FontColor(elAppContainer)); // 글자색
oEditor.registerPlugin(new nhn.husky.SE2M_BGColor(elAppContainer)); // 글자배경색
oEditor.registerPlugin(new nhn.husky.SE2M_FontNameWithLayerUI(elAppContainer, aAdditionalFontList)); // 글꼴종류
oEditor.registerPlugin(new nhn.husky.SE2M_FontSizeWithLayerUI(elAppContainer)); // 글꼴크기
oEditor.registerPlugin(new nhn.husky.SE2M_LineStyler());
oEditor.registerPlugin(new nhn.husky.SE2M_ExecCommand(oWYSIWYGIFrame));
oEditor.registerPlugin(new nhn.husky.SE2M_LineHeightWithLayerUI(elAppContainer)); // 줄간격
oEditor.registerPlugin(new nhn.husky.SE2M_Quote(elAppContainer)); // 인용구
oEditor.registerPlugin(new nhn.husky.SE2M_Hyperlink(elAppContainer)); // 링크
oEditor.registerPlugin(new nhn.husky.SE2M_SCharacter(elAppContainer)); // 특수문자
oEditor.registerPlugin(new nhn.husky.SE2M_FindReplacePlugin(elAppContainer)); // 찾기/바꾸기
oEditor.registerPlugin(new nhn.husky.SE2M_TableCreator(elAppContainer)); // 테이블 생성
oEditor.registerPlugin(new nhn.husky.SE2M_TableEditor(elAppContainer)); // 테이블 편집
oEditor.registerPlugin(new nhn.husky.SE2M_TableBlockStyler(elAppContainer)); // 테이블 스타일
if(nhn.husky.SE2M_AttachQuickPhoto){
oEditor.registerPlugin(new nhn.husky.SE2M_AttachQuickPhoto(elAppContainer)); // 사진
}
oEditor.registerPlugin(new nhn.husky.MessageManager(oMessageMap));
oEditor.registerPlugin(new nhn.husky.SE2M_QuickEditor_Common(elAppContainer)); // 퀵에디터 공통(표, 이미지)
oEditor.registerPlugin(new nhn.husky.SE2B_CSSLoader()); // CSS lazy load
if(window.frameElement){
oEditor.registerPlugin(new nhn.husky.SE_OuterIFrameControl(elAppContainer, 100));
}
oEditor.registerPlugin(new nhn.husky.SE_ToolbarToggler(elAppContainer, htParams.bUseToolbar));
oEditor.registerPlugin(new nhn.husky.SE2M_Accessibility(elAppContainer)); // 에디터내의 웹접근성 관련 기능모음 플러그인
return oEditor;
}
-52
View File
@@ -1,52 +0,0 @@
/*
* Smart Editor 2 Configuration : This setting must be changed by service
*/
window.nhn = window.nhn || {};
nhn.husky = nhn.husky || {};
nhn.husky.SE2M_Configuration = nhn.husky.SE2M_Configuration || {};
/**
* CSS LazyLoad를 위한 경로
*/
nhn.husky.SE2M_Configuration.SE2B_CSSLoader = {
sCSSBaseURI : "css"
};
/**
* 편집영역 설정
*/
nhn.husky.SE2M_Configuration.SE_EditingAreaManager = {
sCSSBaseURI : "css", // smart_editor2_inputarea.html 파일의 상대경로
sBlankPageURL : "smart_editor2_inputarea.html",
sBlankPageURL_EmulateIE7 : "smart_editor2_inputarea_ie8.html",
aAddtionalEmulateIE7 : [] // IE8 default 사용, IE9 ~ 선택적 사용
};
/**
* [웹접근성]
* 단축키 ALT+, ALT+. 을 이용하여 스마트에디터 영역의 이전/이후 요소로 이동할 수 있다.
* sBeforeElementId : 스마트에디터 영역 이전 요소의 id
* sNextElementId : 스마트에디터 영역 이후 요소의 id
*
* 스마트에디터 영역 이외의 제목 영역 (예:스마트에디터가 적용된 블로그 쓰기 페이지에서의 제목 영역) 에 해당하는 엘리먼트에서 Tab키를 누르면 에디팅 영역으로 포커스를 이동시킬 수 있다.
* sTitleElementId : 제목에 해당하는 input 요소의 id.
*/
nhn.husky.SE2M_Configuration.SE2M_Accessibility = {
sBeforeElementId : '',
sNextElementId : '',
sTitleElementId : ''
};
/**
* 링크 기능 옵션
*/
nhn.husky.SE2M_Configuration.SE2M_Hyperlink = {
bAutolink : true // 자동링크기능 사용여부(기본값:true)
};
nhn.husky.SE2M_Configuration.Quote = {
sImageBaseURL : 'http://static.se2.naver.com/static/img'
};
nhn.husky.SE2M_Configuration.SE2M_ColorPalette = {
bAddRecentColorFromDefault : false
};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-40
View File
@@ -1,40 +0,0 @@
SmartEditor Basic 2.0 릴리즈 패키지
SmartEdtitor™는 Javascript로 구현된 웹 기반의 WYSIWYG 에디터입니다. SmartEdtitor™는 WYSIWYG 모드 및 HTML 편집 모드와 TEXT 모드를 제공하고, 자유로운 폰트 크기 설정 기능, 줄 간격 설정 기능, 단어 찾기/바꾸기 기능 등 편집에 필요한 다양한 기능을 제공하므로 사용자들은 SmartEdtitor™를 사용하여 쉽고 편리하게 원하는 형태의 글을 작성할 수 있습니다.
또한, SmartEdtitor™의 구조는 기능을 쉽게 추가할 수 있는 플러그인 구조로 되어 있어 정해진 규칙에 따라 플러그인을 만들기만 하면 됩니다.
현재 SmartEdtitor™는 네이버, 한게임 등 NHN의 주요 서비스에 적용되어 있습니다.
지원하는 브라우저 환경은 아래와 같으며 지속적으로 지원 대상 브라우저를 확장할 예정입니다.
* 지원하는 브라우저
Internet Explorer 7.0+ / 10.0-
FireFox 3.5+
Safari 4.0+
Chrome 4.0+
또한 지속적인 기능 추가를 통해 편리하고 강력한 에디터로 거듭날 것입니다.
라이센스 : LGPL v2
홈페이지 : http://dev.naver.com/projects/smarteditor
===================================================================================
릴리즈 패키지에 포함된 파일은 아래와 같습니다.
/css : 에디터에서 사용하는 css 파일
/img : 에디터에서 사용하는 이미지 파일
/js : 에디터를 적용할 때 사용하는 JS 파일
/photo_uploader : 사진 퀵 업로더 팝업 UI를 구성하는 파일
readme.txt : 간략한 설명
release_notes.txt : 릴리즈 노트
sample.php : SmartEditor2.html을 이용해 편집한 내용을 서버에서 받는 php 예제
smart_editor2_inputarea.html : 에디터의 편집 영역을 나타내는 HTML로 에디터를 적용할 때 반드시 필요
smart_editor2_inputarea_ie8.html : smart_editor2_inputarea.html와 동일한 기능이나 사용자의 브라우저 Internet Explorer 8.x 이상인 경우에 사용
SmartEditor2.html : 에디터 데모 페이지. 에디터 적용 시에도 참고 할 수 있다.
SmartEditor2Skin.html : 에디터를 적용한 페이지에서 로드하는 에디터의 스킨 HTML 파일로 에디터에서 사용하는 JS 파일과 css 파일을 링크하며 에디터의 마크업을 가지고 있다. SmartEditor2.html 에서도 확인할 수 있다.
src_include.txt : 자바스크립트 플러그인 소스를 직접 수정하고자 할 경우 참고할 수 있는 파일
===================================================================================
사용 중 불편한 점이 있거나 버그를 발견하는 경우 SmartEdtitor™ 프로젝트의 이슈에 올려 주세요~~~
http://dev.naver.com/projects/smarteditor/issue
여기입니다! :)
-255
View File
@@ -1,255 +0,0 @@
==============================================================================================
2.3.10_임시
----------------------------------------------------------------------------------------------
1. 버그 수정
- 크롬 > 밑줄 선택 글작성하다 취소선 선택하고 밑줄 선택을 취소한 경우 툴바에 반영되지 않는 문제
- 굵게/밑줄/기울림/취소선이 있는 상태에서 엔터치고 폰트크기 수정하면 이전 폰트크기로 줄간격이 유지되는 문제
- 외부프로그램 테이블 복사 붙여넣기 관련 오류 수정
- IE8이하 > 글자크기 지정 후 엔터를 치면 커서위치가 위로 올라감
- IE9이상 > 글꼴 효과를 미리 지정 한 후에 텍스트 입력 시, 색상 변경은 적용되나 굵게 기울임 밑줄 취소선 등의 효과는 적용안됨
- [FF]밑줄 선택> 내용입력 후 엔터>밑줄 취소 후 내용 입력>마우스로 커서 클릭 후 내용 계속 입력 시 밑줄이 있는 글로 노출됨
- [FF] 메모장에서 작성한 내용을 붙여넣기 후 엔터 > 내용입력 > 엔터 했을 때 줄바꿈이 되지 않는 현상
- HTML5 > 글자를 선택하여 폰트크기 지정시 굵게/밑줄/기울림/취소선이 있으면 이전에 적용한 폰트크기 기준으로 줄간격이 유지되는 문제
2. 기능 개선
- IE에서 자동으로 공백이 삽입되는 문제
- MacOS > 사파리 > 외부프로그램 테이블 붙여넣기 개선
3. 보안 패치
- 사진첨부 샘플의 null byte injection 취약점 보완
==============================================================================================
2.3.10
----------------------------------------------------------------------------------------------
1. 버그 수정
- 크롬 > 브라우저 확대축소시 폰트크기가 잘못 나오는 이슈
- IE > 표삽입>임의로 두개 칸 선택하여 셀 병합>행삽입 클릭 시 JS 오류 발생
- IE11 > 호환성 보기를 설정하지 않을 경우 글꼴목록이 선택되지 않는 문제 수정
2. 기능 개선
- 외부프로그램 테이블 복사 붙여넣기 개선
- 입력창 조절 안내 레이어를 주석처리하면 스크립트 오류 발생
==============================================================================================
2.3.9
----------------------------------------------------------------------------------------------
1. 버그 수정
- 파이어폭스에서 에디팅시 스타일깨짐 등 오작동
- Chrome > 찾기/바꾸기 > 모두바꾸기 버튼 클릭시 찾을단어가 지워지지 않고 남아있음
2. 기능 개선
- 링크 > 자동링크 설정/해제 옵션 추가
- [IE11] WYSIWYG 모드와 HTML 모드를 오갈 때마다 문서의 마지막에 비정상적인 <BR>이 첨가됩니다.
- [웹접근성] 빠져나가기 단축키 기능 개선
==============================================================================================
2.3.8
----------------------------------------------------------------------------------------------
1. 버그 수정
- 테이블 내부 영역을 Shift + 클릭으로 선택 후 정렬하고 HTML 로 전환하면 더미 P 태그가 생성되는 문제 수정
- 테이블 내부 영역 선택 혹은 에디터 내용 전체 선택 후 정렬 시 동작안함
- [IE10, IE11] 표의 셀을 드래그했을 때 블럭 지정이 되지 않는 현상
- HTML 모드 변환시 태그 자동 정렬에 의한 버그
2. 기능 개선
- [MacOS 대응] 폰트변경이슈
==============================================================================================
2.3.7
----------------------------------------------------------------------------------------------
1. 버그 수정
- 에디터에 표 생성 후 일부 셀 선택하여 배경색 설정> 배경색 설정된 셀 선택 후 셀 삽입 시 색상이 삽입되지 않습니다.
- [IE9특정] 글 작성 중 번호매기기 또는 글머리 적용 후 정렬방식을 변경하면 엔터키 누를 시 커서가 한줄 떨어져서 노출됩니다.
- [IE10] 표 생성 후 표 드래그 시 셀의 너비/높이가 늘어나는 현상
2. 기능 개선
- IE11 대응
- 특수기호 삽입시 커서 위치가 뒤쪽으로 나오도록 개선
- 커서에 활성화된 글꼴 확인 로직 개선
==============================================================================================
2.3.6
----------------------------------------------------------------------------------------------
1. 버그 수정
- 글 작성 후 번호매기기 적용하고 엔터키 수행하는 경우 JS 오류가 발생하는 현상 수정
==============================================================================================
2.3.5
----------------------------------------------------------------------------------------------
1. 기능 개선
- 줄간격 설정 시 값을 직접 입력하는 경우 줄간격의 최소값 적용
==============================================================================================
2.3.4
----------------------------------------------------------------------------------------------
1. 버그 수정
- [IE9/10] pre 태그의 바로 다음에 \n이 존재하는 경우 개행이 되지 않는 이슈 해결
- 입력창 크기 조절바 사용 여부 오류 해결
- 사진 퀵 업로더 모듈 오타 수정 ($newPath -> $new_path)
2. 기능 개선
- 글꼴 목록에 글꼴 종류 추가하기 기능 (SmartEditor2.html 참조)
- 사진 퀵 업로더 모듈에 이미지 파일 확장자 체크 추가
==============================================================================================
2.3.3
----------------------------------------------------------------------------------------------
1. 버그 수정
- IE9 에서 템플릿을 적용한 표 생성 후 일부의 셀을 드래그하는 경우 셀의 높이가 늘어나는 현상 수정
2. 기능 개선
- MAC OS의 CMD 키로 Ctrl 단축키 기능 적용 확장
- 기본 글꼴 종류 추가 (Courier New, 나눔고딕 코딩)
==============================================================================================
2.3.1
----------------------------------------------------------------------------------------------
1. 기능 개선
- [웹접근성] 글쓰기 영역의 iframe의 title속성에 단축키 설명 제공
- [웹접근성] 제목 input영역에서 제목 입력 후 TAB하면 스마트에디터 편집 영역으로 포커스 이동하는 기능 추가
- [웹접근성] 툴바 영역의 이전/다음 아이템 이동을 TAB, SHIFT+TAB으로 이동할 수 있도록 추가
==============================================================================================
2.3.0
----------------------------------------------------------------------------------------------
1. 기능 개선
- [웹접근성] 키보드로만 메뉴를 이동할 수 있도록 단축키 적용
- [웹접근성] 웹접근성 도움말 제공
- 편집모드와 사이즈 조절바 사용 옵션 추가
- 사진 첨부 팝업 데모 파일 구조 개선
==============================================================================================
2.2.1
----------------------------------------------------------------------------------------------
1. 버그 수정
- 사진 퀵 업로더 추가 시, 가이드 대로 수행했을 때 사진 첨부가 2번 실행되는 문제 해결
: loader-min.js 파일 내에 사진 퀵 업로더 소스가 포함되어 있던 부분 제거하여 소스 분리
2. 기능 개선
- 툴바의 기능 제거/순서 변경이 쉽도록 마크업 구조 개선
※ 툴바의 기능 제거/순서 변경은 가이드 문서를 참고하세요.
3. 폴더/파일 변경
- /js_src 폴더 제거
- /js/smarteditor2.js 추가
: /js_src 폴더를 /js/smarteditor2.js 로 대체했습니다.
: /js_src 폴더 구조에서 사용자가 소스를 검색하여 수정하기 어렵던 부분을 보완하기 위하여
: /js_src 폴더 내의 플러그인 소스를 통합한 /js/smarteditor2.js 를 추가했습니다.
- /js/loader-min.js 제거
- /js/smarteditor2.min.js 추가
: /js/loader-min.js 파일을 /js/smarteditor2.min.js로 대체했습니다.
- /quick_photo_uploader 폴더 추가
- /popup 폴더 이동
: /popup 폴더 - 사진 퀵 업로더의 팝업과 관련된 소스
: /plugin 폴더 - 사진 퀵 업로더의 사진첨부를 처리하는 플러그인 js 소스
- /img/ko_KR 폴더 추가
: 이후의 다국어 버전 지원을 위하여 이미지 폴더 내 디렉토리가 추가되었습니다.
: 언어 별 구분이 필요없는 이미지는 /img 바로 하위에 두었고,
: 언어 별로 구분되어야 하는 이미지는 /img/ko_KR 과 같이 언어 별 디렉토리로 구분했습니다.
: 버전 업그레이드를 하는 경우 이미지 경로가 변경된 점에 주의하시기 바랍니다.
- /js/SE2B_Configuration.js 제거
- /js/SE2B_Configuration_Service.js 추가
- /js/SE2B_Configuration_General.js 추가
: /js/SE2B_Configuration_Service.js 와 /js/SE2B_Configuration_General.js로 파일 분리했습니다.
: /js/SE2B_Configuration_Service.js 는 적용을 할 때 사용자가 변경할 가능성이 높은 플러그인 설정을 갖고,
: /js/SE2B_Configuration_General.js 는 서비스에 적용할 때 변경할 가능성이 거의 없는 설정입니다.
==============================================================================================
2.1.3
----------------------------------------------------------------------------------------------
1. 버그 수정
- [Chrome] 보기 페이지에 글자색이 설정되어 있는 경우 글 작성 시 내용에 적용한 글자색으로 노출되지 않는 문제 해결
- 엔터 처리가 <BR>로 설정된 경우에도 텍스트 모드에서 모드변경 혹은 글 저장할 때 개행이 <P>로 표시되는 문제 해결
- [IE9] 각주 삽입 시, 하단으로 떨어지는 이슈 해결
- [Chrome] 인용구 밖에 글머리기호/번호매기기가 있을 때 인용구 안에서 글머리기호/번호매기기 시 내용이 인용구 밖으로 나가는 문제 해결
- [IE] IE에서 특정 블로그 글을 복사하여 붙여넣기 했을 때 개행이 제거되는 문제 해결
- 사진을 드래그해서 사이즈를 변경한 후 저장 혹은 HTML모드로 변경하면, 사진 사이즈가 원복되는 현상 해결
- [Chrome/FF/Safari] 스크롤바가 생성되도록 문자입력 후 엔터 클릭하지 않은 상태에서 이미지 하나 삽입 시 이미지에 포커싱이 놓이지 않는 문제 해결
- [IE9 표준] 사진을 스크롤로 일부 가린 상태에서 재편집하여 적용했을 때 계속 가려진 상태인 문제 해결
- FF에서 사진을 여러장 첨부 시 스크롤이 가장 마지막 추가한 사진으로 내려가지 않음 해결
- 호환 모드를 제거하고 사진 첨부 시 에디팅 영역의 커서 주위에 <sub><sup> 태그가 붙어서 글자가 매우 작게 되는 현상 해결
- [IE9] 에디터에 각주 연속으로 입력 시 커서가 각주사이로 이동되는 현상 해결
- 글꼴색/글꼴배경색 더보기에서 글꼴색 선택>다시 다른 색상 선택 후 처음 선택되었던 색상 선택 시 처음 선택색상이 원래 자리에서 삭제되지 않는 현상 해결
- 제공하지 않는 기능인 이모티콘 플러그인 소스 제거
- 플러그인 태그 코드 추가 시 <li> 태그와 <button> 태그 사이에 개행이 있으면 이벤트가 등록되지 않는 현상 해결
2. 기능 개선
- 표 삽입 시 본문 작성 영역 안에 너비 100%로 생성되도록 개선
- 호환모드 설정이 설정 파일 정보에 따라 처리되도록 변경
==============================================================================================
2.1.2
----------------------------------------------------------------------------------------------
1. 버그 수정
- [IE9]Shift+Enter를 여러번 하고 글의 중간의 마지막 글자 다음에서 엔터를 쳤을 때 엔터 위치가 달라지는 현상 수정
- [IE9]메모장에서 붙여 넣기 후 내용 중간의 마지막 글자 다음에서 엔터를 쳤을 때 엔터 위치가 달라지는 현상 수정
- 한 줄 입력 후 색상을 적용하고 내용 중간에서 엔터를 쳤을 때 적용되었던 색상이 풀리던 현상 수정
- 글꼴 레이어를 열었을 때, 샘플 텍스트가 잘못 나오던 현상 수정
- 인용구를 14개까지 중첩하고, 15개부터 경고 창이 나오도록 수정
2. 기능 개선
- 찾기/바꾸기 레이어를 닫았다가 다시 열 때, [바꿀 단어] 입력란이 초기화 되도록 개선
- 찾기/바꾸기 레이어 오픈 시 툴바 버튼 inactive 처리
- 표 추가 레이어의 테이블 색상, 배경 색상의 기본 값을 SmartEditor2Skin.html에서 변경할 수 있도록 함
※주의 : 기존의 html파일에 덮어 씌우게 되면 기본 배경 색상이 다르게 표시됨
따라서 반드시 새로 업데이트 된 html 파일을 사용하기를 권장
임의로 수정하려면 위 파일의 아래 부분의 value를 아래와 같이 변경해야 함
<input id="se2_b_color" name="" type="text" maxlength="7" value="#cccccc" class="input_ty3">
<input id="se2_cellbg" name="" type="text" maxlength="7" value="#ffffff" class="input_ty3">
==============================================================================================
2.1.1
----------------------------------------------------------------------------------------------
1. 기능 추가
- 에디터 로딩 완료 시점에 실행되는 함수 (fOnAppLoad) 정의
2. 버그 수정
- 에디터 초기 Width에 100%가 설정될 수 있도록 수정, minWidth 설정 추가
- 마크업에서 나눔 글꼴을 제외하면 JS 에러가 나는 문제 수정
- [IE9] 글자 색상 적용 후 내용 중간에서 계속 Enter할 때 Enter가 되지 않는 오류 수정
- [Chrome/Safari] 표 간단편집기 위에서 text를 drag하면 JS 에러가 발생하는 문제 수정
3. 기능 개선
- 사진 퀵 업로더 : 쉽게 사용할 수 있도록 소스 수정 및 예제 보강
==============================================================================================
2.1.0
----------------------------------------------------------------------------------------------
1. 기능 추가
- 사진 퀵 업로더 : 사진 첨부 팝업 UI 제공 (HTML5 지원)
- 에디터 본문에 글 작성 후 창을 닫을 때 발생하는 alert 메세지를 사용자가 설정할 수 있도록 옵션을 추가함
- Jindo 모듈을 패키지에 포함하도록 빌드를 수정함
- document.domain을 제거함
- 에디터 초기 Width를 설정할 수 있도록 수정함
- 툴바의 접힘/펼침 기능을 제공하는 SE_ToolbarToggler 플러그인 추가함
2. 버그 수정
- 에디터 리사이즈 시 북마크 태그가 본문에 추가되는 이슈 확인 및 수정함
==============================================================================================
2.0.0
----------------------------------------------------------------------------------------------
1. 기능 강화
- 글꼴과 글자 크기
: 기존의 Selectbox 형태의 글꼴 목록을 깔끔한 디자인의 레이어로 제공한다.
- 글자색과 글자 배경색
: 기존의 기본 색상표 이외에 다양한 색상을 선택할 수 있는 컬러 팔레트를 확장 지원한다.
- 줄간격
: 기존의 Selectbox 형태의 줄간격 목록을 깔끔한 디자인의 레이어로 제공한다.
또한, 줄간격을 직접 설정할 수 있도록 직접 입력 기능도 확장 지원한다.
- 인용구
: 기존의 7가지에서 10가지로 인용구 디자인을 확장 지원한다.
- 표
: 표 생성 시 기존의 테두리 색상과 두께를 설정할 수 있는 기능 이외에 테두리 스타일을 설정할 수 있는 기능을 확장 지원한다.
또한, 표 템플릿을 제공하여 보다 쉽게 표 스타일을 생성할 수 있도록 하였다.
2. 기능 추가
- 표 간단편집기
: 표 생성 후 스타일을 편집할 수 있도록 표 편집 기능을 추가 제공한다.
- TEXT 모드
: WYSIWYG와 HTML 모드 이외에 TEXT 모드를 제공하여 텍스트만으로 본문의 내용을 작성할 수 있도록 편집 모드를 추가 제공한다.
-33
View File
@@ -1,33 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="ko" xml:lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Smart Editor&#8482; 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('&lt;img id="test" width="0" height="0"&gt;'))?>
<script>
if(!document.getElementById("test")) {
alert("PHP가 실행되지 않았습니다. 내용을 로컬 파일로 전송한 것이 아니라 서버로 전송했는지 확인 해 주십시오.");
}
</script>
</body>
</html>
@@ -1,107 +0,0 @@
/**
* @use 간단 포토 업로드용으로 제작되었습니다.
* @author cielo
* @See nhn.husky.SE2M_Configuration
* @ 팝업 마크업은 SimplePhotoUpload.html과 SimplePhotoUpload_html5.html이 있습니다.
*/
nhn.husky.SE2M_AttachQuickPhoto = jindo.$Class({
name : "SE2M_AttachQuickPhoto",
$init : function(){},
$ON_MSG_APP_READY : function(){
this.oApp.exec("REGISTER_UI_EVENT", ["photo_attach", "click", "ATTACHPHOTO_OPEN_WINDOW"]);
},
$LOCAL_BEFORE_FIRST : function(sMsg){
if(!!this.oPopupMgr){ return; }
// Popup Manager에서 사용할 param
this.htPopupOption = {
oApp : this.oApp,
sName : this.name,
bScroll : false,
sProperties : "",
sUrl : ""
};
this.oPopupMgr = nhn.husky.PopUpManager.getInstance(this.oApp);
},
/**
* 포토 웹탑 오픈
*/
$ON_ATTACHPHOTO_OPEN_WINDOW : function(){
this.htPopupOption.sUrl = this.makePopupURL();
this.htPopupOption.sProperties = "left=0,top=0,width=403,height=359,scrollbars=yes,location=no,status=0,resizable=no";
this.oPopupWindow = this.oPopupMgr.openWindow(this.htPopupOption);
// 처음 로딩하고 IE에서 커서가 전혀 없는 경우
// 복수 업로드시에 순서가 바뀜
// [SMARTEDITORSUS-1698]
this.oApp.exec('FOCUS', [true]);
// --[SMARTEDITORSUS-1698]
return (!!this.oPopupWindow ? true : false);
},
/**
* 서비스별로 팝업에 parameter를 추가하여 URL을 생성하는 함수
* nhn.husky.SE2M_AttachQuickPhoto.prototype.makePopupURL로 덮어써서 사용하시면 됨.
*/
makePopupURL : function(){
var sPopupUrl = "./sample/photo_uploader/photo_uploader.html";
return sPopupUrl;
},
/**
* 팝업에서 호출되는 메세지.
*/
$ON_SET_PHOTO : function(aPhotoData){
var sContents,
aPhotoInfo,
htData;
if( !aPhotoData ){
return;
}
try{
sContents = "";
for(var i = 0; i <aPhotoData.length; i++){
htData = aPhotoData[i];
if(!htData.sAlign){
htData.sAlign = "";
}
aPhotoInfo = {
sName : htData.sFileName || "",
sOriginalImageURL : htData.sFileURL,
bNewLine : htData.bNewLine || false
};
sContents += this._getPhotoTag(aPhotoInfo);
}
this.oApp.exec("PASTE_HTML", [sContents]); // 위즐 첨부 파일 부분 확인
}catch(e){
// upload시 error발생에 대해서 skip함
return false;
}
},
/**
* @use 일반 포토 tag 생성
*/
_getPhotoTag : function(htPhotoInfo){
// id와 class는 썸네일과 연관이 많습니다. 수정시 썸네일 영역도 Test
var sTag = '<img src="{=sOriginalImageURL}" title="{=sName}" >';
if(htPhotoInfo.bNewLine){
sTag += '<br style="clear:both;">';
}
sTag = jindo.$Template(sTag).process(htPhotoInfo);
return sTag;
}
});
@@ -1,684 +0,0 @@
//변수 선언 및 초기화
var nImageInfoCnt = 0;
var htImageInfo = []; //image file정보 저장
var aResult = [];
var rFilter = /^(image\/bmp|image\/gif|image\/jpg|image\/jpeg|image\/png)$/i;
var rFilter2 = /^(bmp|gif|jpg|jpeg|png)$/i;
var nTotalSize = 0;
var nMaxImageSize = 10*1024*1024;
var nMaxTotalImageSize = 50*1024*1024;
var nMaxImageCount = 10;
var nImageFileCount = 0;
var bSupportDragAndDropAPI = false;
var oFileUploader;
var bAttachEvent = false;
//마크업에 따른 할당
var elContent= $("pop_content");
var elDropArea = jindo.$$.getSingle(".drag_area",elContent);
var elDropAreaUL = jindo.$$.getSingle(".lst_type",elContent);
var elCountTxtTxt = jindo.$$.getSingle("#imageCountTxt",elContent);
var elTotalSizeTxt = jindo.$$.getSingle("#totalSizeTxt",elContent);
var elTextGuide = $("guide_text");
var welUploadInputBox = $Element("uploadInputBox");
var oNavigator = jindo.$Agent().navigator();
//마크업-공통
var welBtnConfirm = $Element("btn_confirm"); //확인 버튼
var welBtnCancel= $Element("btn_cancel"); //취소 버튼
//진도로 랩핑된 element
var welTextGuide = $Element(elTextGuide);
var welDropArea = $Element(elDropArea);
var welDropAreaUL = $Element(elDropAreaUL);
var fnUploadImage = null;
//File API 지원 여부로 결정
function checkDragAndDropAPI(){
try{
if( !oNavigator.ie ){
if(!!oNavigator.safari && oNavigator.version <= 5){
bSupportDragAndDropAPI = false;
}else{
bSupportDragAndDropAPI = true;
}
} else {
bSupportDragAndDropAPI = false;
}
}catch(e){
bSupportDragAndDropAPI = false;
}
}
//--------------- html5 미지원 브라우저에서 (IE9 이하) ---------------
/**
* 이미지를 첨부 후 활성화된 버튼 상태
*/
function goStartMode(){
var sSrc = welBtnConfirm.attr("src")|| "";
if(sSrc.indexOf("btn_confirm2.png") < 0 ){
welBtnConfirm.attr("src","./img/btn_confirm2.png");
fnUploadImage.attach(welBtnConfirm.$value(), "click");
}
}
/**
* 이미지를 첨부 전 비활성화된 버튼 상태
* @return
*/
function goReadyMode(){
var sSrc = welBtnConfirm.attr("src")|| "";
if(sSrc.indexOf("btn_confirm2.png") >= 0 ){
fnUploadImage.detach(welBtnConfirm.$value(), "click");
welBtnConfirm.attr("src","./img/btn_confirm.png");
}
}
/**
* 일반 업로드
* @desc oFileUploader의 upload함수를 호출함.
*/
function generalUpload(){
oFileUploader.upload();
}
/**
* 이미지 첨부 전 안내 텍스트가 나오는 배경으로 '설정'하는 함수.
* @return
*/
function readyModeBG (){
var sClass = welTextGuide.className();
if(sClass.indexOf('nobg') >= 0){
welTextGuide.removeClass('nobg');
welTextGuide.className('bg');
}
}
/**
* 이미지 첨부 전 안내 텍스트가 나오는 배경을 '제거'하는 함수.
* @return
*/
function startModeBG (){
var sClass = welTextGuide.className();
if(sClass.indexOf('nobg') < 0){
welTextGuide.removeClass('bg');
welTextGuide.className('nobg');
}
}
//--------------------- html5 지원되는 브라우저에서 사용하는 함수 --------------------------
/**
* 팝업에 노출될 업로드 예정 사진의 수.
* @param {Object} nCount 현재 업로드 예정인 사진 장수
* @param {Object} nVariable 삭제되는 수
*/
function updateViewCount (nCount, nVariable){
var nCnt = nCount + nVariable;
elCountTxtTxt.innerHTML = nCnt +"장";
nImageFileCount = nCnt;
return nCnt;
}
/**
* 팝업에 노출될 업로드될 사진 총 용량
*/
function updateViewTotalSize(){
var nViewTotalSize = Number(parseInt((nTotalSize || 0), 10) / (1024*1024));
elTotalSizeTxt.innerHTML = nViewTotalSize.toFixed(2) +"MB";
}
/**
* 이미지 전체 용량 재계산.
* @param {Object} sParentId
*/
function refreshTotalImageSize(sParentId){
var nDelImgSize = htImageInfo[sParentId].size;
if(nTotalSize - nDelImgSize > -1 ){
nTotalSize = nTotalSize - nDelImgSize;
}
}
/**
* hash table에서 이미지 정보 초기화.
* @param {Object} sParentId
*/
function removeImageInfo (sParentId){
//삭제된 이미지의 공간을 초기화 한다.
htImageInfo[sParentId] = null;
}
/**
* byte로 받은 이미지 용량을 화면에 표시를 위해 포맷팅
* @param {Object} nByte
*/
function setUnitString (nByte) {
var nImageSize;
var sUnit;
if(nByte < 0 ){
nByte = 0;
}
if( nByte < 1024) {
nImageSize = Number(nByte);
sUnit = 'B';
return nImageSize + sUnit;
} else if( nByte > (1024*1024)) {
nImageSize = Number(parseInt((nByte || 0), 10) / (1024*1024));
sUnit = 'MB';
return nImageSize.toFixed(2) + sUnit;
} else {
nImageSize = Number(parseInt((nByte || 0), 10) / 1024);
sUnit = 'KB';
return nImageSize.toFixed(0) + sUnit;
}
}
/**
* 화면 목록에 적당하게 이름을 잘라서 표시.
* @param {Object} sName 파일명
* @param {Object} nMaxLng 최대 길이
*/
function cuttingNameByLength (sName, nMaxLng) {
var sTemp, nIndex;
if(sName.length > nMaxLng){
nIndex = sName.indexOf(".");
sTemp = sName.substring(0,nMaxLng) + "..." + sName.substring(nIndex,sName.length) ;
} else {
sTemp = sName;
}
return sTemp;
}
/**
* Total Image Size를 체크해서 추가로 이미지를 넣을지 말지를 결정함.
* @param {Object} nByte
*/
function checkTotalImageSize(nByte){
if( nTotalSize + nByte < nMaxTotalImageSize){
nTotalSize = nTotalSize + nByte;
return false;
} else {
return true;
}
}
// 이벤트 핸들러 할당
function dragEnter(ev) {
ev.stopPropagation();
ev.preventDefault();
}
function dragExit(ev) {
ev.stopPropagation();
ev.preventDefault();
}
function dragOver(ev) {
ev.stopPropagation();
ev.preventDefault();
}
/**
* 드랍 영역에 사진을 떨구는 순간 발생하는 이벤트
* @param {Object} ev
*/
function drop(ev) {
ev.stopPropagation();
ev.preventDefault();
if (nImageFileCount >= 10){
alert("최대 10장까지만 등록할 수 있습니다.");
return;
}
if(typeof ev.dataTransfer.files == 'undefined'){
alert("HTML5를 지원하지 않는 브라우저입니다.");
}else{
//변수 선언
var wel,
files,
nCount,
sListTag = '';
//초기화
files = ev.dataTransfer.files;
nCount = files.length;
if (!!files && nCount === 0){
//파일이 아닌, 웹페이지에서 이미지를 드래서 놓는 경우.
alert("정상적인 첨부방식이 아닙니다.");
return ;
}
for (var i = 0, j = nImageFileCount ; i < nCount ; i++){
if (!rFilter.test(files[i].type)) {
alert("이미지파일 (jpg,gif,png,bmp)만 업로드 가능합니다.");
} else if(files[i].size > nMaxImageSize){
alert("이미지 용량이 10MB를 초과하여 등록할 수 없습니다.");
} else {
//제한된 수만 업로드 가능.
if ( j < nMaxImageCount ){
sListTag += addImage(files[i]);
//다음 사진을위한 셋팅
j = j+1;
nImageInfoCnt = nImageInfoCnt+1;
} else {
alert("최대 10장까지만 등록할 수 있습니다.");
break;
}
}
}
if(j > 0){
//배경 이미지 변경
startModeBG();
if ( sListTag.length > 1){
welDropAreaUL.prependHTML(sListTag);
}
//이미지 총사이즈 view update
updateViewTotalSize();
//이미치 총 수 view update
nImageFileCount = j;
updateViewCount(nImageFileCount, 0);
// 저장 버튼 활성화
goStartMode();
}else{
readyModeBG();
}
}
}
/**
* 이미지를 추가하기 위해서 file을 저장하고, 목록에 보여주기 위해서 string을 만드는 함수.
* @param ofile 한개의 이미지 파일
* @return
*/
function addImage(ofile){
//파일 사이즈
var ofile = ofile,
sFileSize = 0,
sFileName = "",
sLiTag = "",
bExceedLimitTotalSize = false,
aFileList = [];
sFileSize = setUnitString(ofile.size);
sFileName = cuttingNameByLength(ofile.name, 15);
bExceedLimitTotalSize = checkTotalImageSize(ofile.size);
if( !!bExceedLimitTotalSize ){
alert("전체 이미지 용량이 50MB를 초과하여 등록할 수 없습니다. \n\n (파일명 : "+sFileName+", 사이즈 : "+sFileSize+")");
} else {
//이미지 정보 저장
htImageInfo['img'+nImageInfoCnt] = ofile;
//List 마크업 생성하기
aFileList.push(' <li id="img'+nImageInfoCnt+'" class="imgLi"><span>'+ sFileName +'</span>');
aFileList.push(' <em>'+ sFileSize +'</em>');
aFileList.push(' <a onclick="delImage(\'img'+nImageInfoCnt+'\')"><img class="del_button" src="./img/btn_del.png" width="14" height="13" alt="첨부 사진 삭제"></a>');
aFileList.push(' </li> ');
sLiTag = aFileList.join(" ");
aFileList = [];
}
return sLiTag;
}
/**
* HTML5 DragAndDrop으로 사진을 추가하고, 확인버튼을 누른 경우에 동작한다.
* @return
*/
function html5Upload() {
var tempFile,
sUploadURL;
//sUploadURL= 'file_uploader_html5.php'; //upload URL
sUploadURL= 'file_uploader_html5.jsp'; //upload URL
//파일을 하나씩 보내고, 결과를 받음.
for(var j=0, k=0; j < nImageInfoCnt; j++) {
tempFile = htImageInfo['img'+j];
try{
if(!!tempFile){
//Ajax통신하는 부분. 파일과 업로더할 url을 전달한다.
callAjaxForHTML5(tempFile,sUploadURL);
k += 1;
}
}catch(e){}
tempFile = null;
}
}
function callAjaxForHTML5 (tempFile, sUploadURL){
var oAjax = jindo.$Ajax(sUploadURL, {
type: 'xhr',
method : "post",
onload : function(res){ // 요청이 완료되면 실행될 콜백 함수
var sResString = res._response.responseText;
if (res.readyState() == 4) {
if(sResString.indexOf("NOTALLOW_") > -1){
var sFileName = sResString.replace("NOTALLOW_", "");
alert("이미지 파일(jpg,gif,png,bmp)만 업로드 하실 수 있습니다. ("+sFileName+")");
}else{
//성공 시에 responseText를 가지고 array로 만드는 부분.
makeArrayFromString(res._response.responseText);
}
}
},
timeout : 3,
onerror : jindo.$Fn(onAjaxError, this).bind()
});
oAjax.header("contentType","multipart/form-data");
oAjax.header("file-name",encodeURIComponent(tempFile.name));
oAjax.header("file-size",tempFile.size);
oAjax.header("file-Type",tempFile.type);
oAjax.request(tempFile);
}
function makeArrayFromString(sResString){
var aTemp = [],
aSubTemp = [],
htTemp = {}
aResultleng = 0;
try{
if(!sResString || sResString.indexOf("sFileURL") < 0){
return ;
}
aTemp = sResString.split("&");
for (var i = 0; i < aTemp.length ; i++){
if( !!aTemp[i] && aTemp[i] != "" && aTemp[i].indexOf("=") > 0){
aSubTemp = aTemp[i].split("=");
htTemp[aSubTemp[0]] = aSubTemp[1];
}
}
}catch(e){}
aResultleng = aResult.length;
aResult[aResultleng] = htTemp;
if(aResult.length == nImageFileCount){
setPhotoToEditor(aResult);
aResult = null;
window.close();
}
}
/**
* 사진 삭제 시에 호출되는 함수
* @param {Object} sParentId
*/
function delImage (sParentId){
var elLi = jindo.$$.getSingle("#"+sParentId);
refreshTotalImageSize(sParentId);
updateViewTotalSize();
updateViewCount(nImageFileCount,-1);
//사진 file array에서 정보 삭제.
removeImageInfo(sParentId);
//해당 li삭제
$Element(elLi).leave();
//마지막 이미지인경우.
if(nImageFileCount === 0){
readyModeBG();
//사진 추가 버튼 비활성화
goReadyMode();
}
// drop 영역 이벤트 다시 활성화.
if(!bAttachEvent){
addEvent();
}
}
/**
* 이벤트 할당
*/
function addEvent() {
bAttachEvent = true;
elDropArea.addEventListener("dragenter", dragEnter, false);
elDropArea.addEventListener("dragexit", dragExit, false);
elDropArea.addEventListener("dragover", dragOver, false);
elDropArea.addEventListener("drop", drop, false);
}
function removeEvent(){
bAttachEvent = false;
elDropArea.removeEventListener("dragenter", dragEnter, false);
elDropArea.removeEventListener("dragexit", dragExit, false);
elDropArea.removeEventListener("dragover", dragOver, false);
elDropArea.removeEventListener("drop", drop, false);
}
/**
* Ajax 통신 시 error가 발생할 때 처리하는 함수입니다.
* @return
*/
function onAjaxError (){
alert("[가이드]사진 업로더할 서버URL셋팅이 필요합니다.-onAjaxError");
}
/**
* 이미지 업로드 시작
* 확인 버튼 클릭하면 호출되는 msg
*/
function uploadImage (e){
if(!bSupportDragAndDropAPI){
generalUpload();
}else{
html5Upload();
}
}
/**
* jindo에 파일 업로드 사용.(iframe에 Form을 Submit하여 리프레시없이 파일을 업로드하는 컴포넌트)
*/
function callFileUploader (){
oFileUploader = new jindo.FileUploader(jindo.$("uploadInputBox"),{
// sUrl : location.href.replace(/\/[^\/]*$/, '') + '/file_uploader.php', //샘플 URL입니다.
// sCallback : location.href.replace(/\/[^\/]*$/, '') + '/callback.html', //업로드 이후에 iframe이 redirect될 콜백페이지의 주소
sUrl : '/imageUpload.do', //파일업로드를 처리하는 페이지
sCallback : '/SE2/sample/photo_uploader/callback.html', //업로드 이후에 iframe이 redirect될 콜백페이지의 주소
sFiletype : "*.jpg;*.png;*.bmp;*.gif", //허용할 파일의 형식. ex) "*", "*.*", "*.jpg", 구분자(;)
sMsgNotAllowedExt : 'JPG, GIF, PNG, BMP 확장자만 가능합니다', //허용할 파일의 형식이 아닌경우에 띄워주는 경고창의 문구
bAutoUpload : false, //파일이 선택됨과 동시에 자동으로 업로드를 수행할지 여부 (upload 메소드 수행)
bAutoReset : true // 업로드한 직후에 파일폼을 리셋 시킬지 여부 (reset 메소드 수행)
}).attach({
select : function(oCustomEvent) {
//파일 선택이 완료되었을 때 발생
// oCustomEvent (이벤트 객체) = {
// sValue (String) 선택된 File Input의 값
// bAllowed (Boolean) 선택된 파일의 형식이 허용되는 형식인지 여부
// sMsgNotAllowedExt (String) 허용되지 않는 파일 형식인 경우 띄워줄 경고메세지
// }
// 선택된 파일의 형식이 허용되는 경우만 처리
if(oCustomEvent.bAllowed === true){
goStartMode();
}else{
goReadyMode();
oFileUploader.reset();
}
// bAllowed 값이 false인 경우 경고문구와 함께 alert 수행
// oCustomEvent.stop(); 수행시 bAllowed 가 false이더라도 alert이 수행되지 않음
},
success : function(oCustomEvent) {
// alert("success");
// 업로드가 성공적으로 완료되었을 때 발생
// oCustomEvent(이벤트 객체) = {
// htResult (Object) 서버에서 전달해주는 결과 객체 (서버 설정에 따라 유동적으로 선택가능)
// }
var aResult = [];
aResult[0] = oCustomEvent.htResult;
setPhotoToEditor(aResult);
//버튼 비활성화
goReadyMode();
oFileUploader.reset();
window.close();
},
error : function(oCustomEvent) {
//업로드가 실패했을 때 발생
//oCustomEvent(이벤트 객체) = {
// htResult : { (Object) 서버에서 전달해주는 결과 객체. 에러발생시 errstr 프로퍼티를 반드시 포함하도록 서버 응답을 설정하여야한다.
// errstr : (String) 에러메시지
// }
//}
//var wel = jindo.$Element("info");
//wel.html(oCustomEvent.htResult.errstr);
alert(oCustomEvent.htResult.errstr);
}
});
}
/**
* 페이지 닫기 버튼 클릭
*/
function closeWindow(){
if(bSupportDragAndDropAPI){
removeEvent();
}
window.close();
}
window.onload = function(){
checkDragAndDropAPI();
if(bSupportDragAndDropAPI){
$Element("pop_container2").hide();
$Element("pop_container").show();
welTextGuide.removeClass("nobg");
welTextGuide.className("bg");
addEvent();
} else {
$Element("pop_container").hide();
$Element("pop_container2").show();
callFileUploader();
}
fnUploadImage = $Fn(uploadImage,this);
$Fn(closeWindow,this).attach(welBtnCancel.$value(), "click");
};
/**
* 서버로부터 받은 데이타를 에디터에 전달하고 창을 닫음.
* @parameter aFileInfo [{},{},...]
* @ex aFileInfo = [
* {
sFileName : "nmms_215646753.gif",
sFileURL :"http://static.naver.net/www/u/2010/0611/nmms_215646753.gif",
bNewLine : true
},
{
sFileName : "btn_sch_over.gif",
sFileURL :"http://static1.naver.net/w9/btn_sch_over.gif",
bNewLine : true
}
* ]
*/
function setPhotoToEditor(oFileInfo){
if (!!opener && !!opener.nhn && !!opener.nhn.husky && !!opener.nhn.husky.PopUpManager) {
//스마트 에디터 플러그인을 통해서 넣는 방법 (oFileInfo는 Array)
opener.nhn.husky.PopUpManager.setCallback(window, 'SET_PHOTO', [oFileInfo]);
//본문에 바로 tag를 넣는 방법 (oFileInfo는 String으로 <img src=....> )
//opener.nhn.husky.PopUpManager.setCallback(window, 'PASTE_HTML', [oFileInfo]);
}
}
// 2012.05 현재] jindo.$Ajax.prototype.request에서 file과 form을 지원하지 안함.
jindo.$Ajax.prototype.request = function(oData) {
this._status++;
var t = this;
var req = this._request;
var opt = this._options;
var data, v,a = [], data = "";
var _timer = null;
var url = this._url;
this._is_abort = false;
if( opt.postBody && opt.type.toUpperCase()=="XHR" && opt.method.toUpperCase()!="GET"){
if(typeof oData == 'string'){
data = oData;
}else{
data = jindo.$Json(oData).toString();
}
}else if (typeof oData == "undefined" || !oData) {
data = null;
} else {
data = oData;
}
req.open(opt.method.toUpperCase(), url, opt.async);
if (opt.sendheader) {
if(!this._headers["Content-Type"]){
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
}
req.setRequestHeader("charset", "utf-8");
for (var x in this._headers) {
if(this._headers.hasOwnProperty(x)){
if (typeof this._headers[x] == "function")
continue;
req.setRequestHeader(x, String(this._headers[x]));
}
}
}
var navi = navigator.userAgent;
if(req.addEventListener&&!(navi.indexOf("Opera") > -1)&&!(navi.indexOf("MSIE") > -1)){
/*
* opera 10.60에서 XMLHttpRequest에 addEventListener기 추가되었지만 정상적으로 동작하지 않아 opera는 무조건 dom1방식으로 지원함.
* IE9에서도 opera와 같은 문제가 있음.
*/
if(this._loadFunc){ req.removeEventListener("load", this._loadFunc, false); }
this._loadFunc = function(rq){
clearTimeout(_timer);
_timer = undefined;
t._onload(rq);
}
req.addEventListener("load", this._loadFunc, false);
}else{
if (typeof req.onload != "undefined") {
req.onload = function(rq){
if(req.readyState == 4 && !t._is_abort){
clearTimeout(_timer);
_timer = undefined;
t._onload(rq);
}
};
} else {
/*
* IE6에서는 onreadystatechange가 동기적으로 실행되어 timeout이벤트가 발생안됨.
* 그래서 interval로 체크하여 timeout이벤트가 정상적으로 발생되도록 수정. 비동기 방식일때만
*/
if(window.navigator.userAgent.match(/(?:MSIE) ([0-9.]+)/)[1]==6&&opt.async){
var onreadystatechange = function(rq){
if(req.readyState == 4 && !t._is_abort){
if(_timer){
clearTimeout(_timer);
_timer = undefined;
}
t._onload(rq);
clearInterval(t._interval);
t._interval = undefined;
}
};
this._interval = setInterval(onreadystatechange,300);
}else{
req.onreadystatechange = function(rq){
if(req.readyState == 4){
clearTimeout(_timer);
_timer = undefined;
t._onload(rq);
}
};
}
}
}
req.send(data);
return this;
};
@@ -1,31 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>FileUploader Callback</title>
</head>
<body>
<script type="text/javascript">
// alert("callback");
// document.domain 설정
try { document.domain = "http://localhost"; } catch(e) {}
// execute callback script
var sUrl = document.location.search.substr(1);
if (sUrl != "blank") {
var oParameter = {}; // query array
sUrl.replace(/([^=]+)=([^&]*)(&|$)/g, function(){
oParameter[arguments[1]] = arguments[2];
return "";
});
if ((oParameter.errstr || '').length) { // on error
(parent.jindo.FileUploader._oCallback[oParameter.callback_func+'_error'])(oParameter);
} else {
(parent.jindo.FileUploader._oCallback[oParameter.callback_func+'_success'])(oParameter);
}
}
</script>
</body>
</html>
@@ -1,96 +0,0 @@
<%--------------------------------------------------------------------------------
* 화면명 : Smart Editor 2.8 에디터 - 싱글 파일 업로드 처리
* 파일명 : /SE2/sample/photo_uploader/file_uploader.jsp
--------------------------------------------------------------------------------%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="java.util.List"%>
<%@ page import="java.util.UUID"%>
<%@ page import="java.io.File"%>
<%@ page import="java.io.FileOutputStream"%>
<%@ page import="java.io.InputStream"%>
<%@ page import="java.io.OutputStream"%>
<%@ page import="org.apache.commons.fileupload.FileItem"%>
<%@ page import="org.apache.commons.fileupload.disk.DiskFileItemFactory"%>
<%@ page import="org.apache.commons.fileupload.servlet.ServletFileUpload"%>
<%
// 로컬경로에 파일 저장하기 ============================================
String return1 = "";
String return2 = "";
String return3 = "";
String name = "";
// multipart로 전송되었는가 체크
if(ServletFileUpload.isMultipartContent(request)) {
ServletFileUpload uploadHandler = new ServletFileUpload(new DiskFileItemFactory());
// UTF-8 인코딩 설정
uploadHandler.setHeaderEncoding("UTF-8");
List<FileItem> items = uploadHandler.parseRequest(request);
// 각 필드태그들을 FOR문을 이용하여 비교를 합니다.
for(FileItem item : items) {
if(item.getFieldName().equals("callback")) {
return1 = item.getString("UTF-8");
} else if(item.getFieldName().equals("callback_func")) {
return2 = "?callback_func="+item.getString("UTF-8");
} else if(item.getFieldName().equals("Filedata")) {
// FILE 태그가 1개이상일 경우
if(item.getSize() > 0) {
// 확장자
String ext = item.getName().substring(item.getName().lastIndexOf(".")+1);
// 파일 기본경로
String defaultPath = request.getServletContext().getRealPath("/");
// 파일 기본경로 _ 상세경로
String path = defaultPath + "upload" + File.separator;
File file = new File(path);
// 디렉토리 존재하지 않을경우 디렉토리 생성
if(!file.exists()) {
file.mkdirs();
}
// 서버에 업로드 할 파일명(한글문제로 인해 원본파일은 올리지 않는것이 좋음)
String realname = UUID.randomUUID().toString() + "." + ext;
///////////////// 서버에 파일쓰기 /////////////////
InputStream is = item.getInputStream();
OutputStream os=new FileOutputStream(path + realname);
int numRead;
byte b[] = new byte[(int)item.getSize()];
while((numRead = is.read(b,0,b.length)) != -1) {
os.write(b,0,numRead);
}
if(is != null) is.close();
os.flush();
os.close();
System.out.println("path : "+path);
System.out.println("realname : "+realname);
// 파일 삭제
// File f1 = new File(path, realname);
// if (!f1.isDirectory()) {
// if(!f1.delete()) {
// System.out.println("File 삭제 오류!");
// }
// }
///////////////// 서버에 파일쓰기 /////////////////
return3 += "&bNewLine=true&sFileName="+name+"&sFileURL=/upload/"+realname;
} else {
return3 += "&errstr=error";
}
}
}
}
response.sendRedirect(return1+return2+return3);
// ./로컬경로에 파일 저장하기 ============================================
%>
@@ -1,37 +0,0 @@
<?php
// default redirection
$url = $_REQUEST["callback"].'?callback_func='.$_REQUEST["callback_func"];
$bSuccessUpload = is_uploaded_file($_FILES['Filedata']['tmp_name']);
// SUCCESSFUL
if(bSuccessUpload) {
$tmp_name = $_FILES['Filedata']['tmp_name'];
$name = $_FILES['Filedata']['name'];
$filename_ext = strtolower(array_pop(explode('.',$name)));
$allow_file = array("jpg", "png", "bmp", "gif");
if(!in_array($filename_ext, $allow_file)) {
$url .= '&errstr='.$name;
} else {
$uploadDir = '../../upload/';
if(!is_dir($uploadDir)){
mkdir($uploadDir, 0777);
}
$newPath = $uploadDir.urlencode($_FILES['Filedata']['name']);
@move_uploaded_file($tmp_name, $newPath);
$url .= "&bNewLine=true";
$url .= "&sFileName=".urlencode(urlencode($name));
$url .= "&sFileURL=upload/".urlencode(urlencode($name));
}
}
// FAILED
else {
$url .= '&errstr=error';
}
header('Location: '. $url);
?>
@@ -1,69 +0,0 @@
<%--------------------------------------------------------------------------------
* 화면명 : Smart Editor 2.8 에디터 - 다중 파일 업로드 처리
* 파일명 : /SE2/sample/photo_uploader/file_uploader_html5.jsp
--------------------------------------------------------------------------------%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="java.util.List"%>
<%@ page import="java.util.UUID"%>
<%@ page import="java.io.File"%>
<%@ page import="java.io.FileOutputStream"%>
<%@ page import="java.io.InputStream"%>
<%@ page import="java.io.OutputStream"%>
<%@ page import="org.apache.commons.fileupload.FileItem"%>
<%@ page import="org.apache.commons.fileupload.disk.DiskFileItemFactory"%>
<%@ page import="org.apache.commons.fileupload.servlet.ServletFileUpload"%>
<%
// 로컬경로에 파일 저장하기 ============================================
String sFileInfo = "";
// 파일명 - 싱글파일업로드와 다르게 멀티파일업로드는 HEADER로 넘어옴
String name = request.getHeader("file-name");
// 확장자
String ext = name.substring(name.lastIndexOf(".")+1);
// 파일 기본경로
String defaultPath = request.getServletContext().getRealPath("/");
// 파일 기본경로 _ 상세경로
String path = defaultPath + "upload" + File.separator;
File file = new File(path);
if(!file.exists()) {
file.mkdirs();
}
String realname = UUID.randomUUID().toString() + "." + ext;
InputStream is = request.getInputStream();
OutputStream os = new FileOutputStream(path + realname);
int numRead;
// 파일쓰기
byte b[] = new byte[Integer.parseInt(request.getHeader("file-size"))];
while((numRead = is.read(b,0,b.length)) != -1) {
os.write(b,0,numRead);
}
if(is != null) {
is.close();
}
os.flush();
os.close();
System.out.println("path : "+path);
System.out.println("realname : "+realname);
// 파일 삭제
// File f1 = new File(path, realname);
// if (!f1.isDirectory()) {
// if(!f1.delete()) {
// System.out.println("File 삭제 오류!");
// }
// }
sFileInfo += "&bNewLine=true&sFileName="+ name+"&sFileURL="+"/upload/"+realname;
out.println(sFileInfo);
// ./로컬경로에 파일 저장하기 ============================================
%>
@@ -1,38 +0,0 @@
<?php
$sFileInfo = '';
$headers = array();
foreach($_SERVER as $k => $v) {
if(substr($k, 0, 9) == "HTTP_FILE") {
$k = substr(strtolower($k), 5);
$headers[$k] = $v;
}
}
$file = new stdClass;
$file->name = str_replace("\0", "", rawurldecode($headers['file_name']));
$file->size = $headers['file_size'];
$file->content = file_get_contents("php://input");
$filename_ext = strtolower(array_pop(explode('.',$file->name)));
$allow_file = array("jpg", "png", "bmp", "gif");
if(!in_array($filename_ext, $allow_file)) {
echo "NOTALLOW_".$file->name;
} else {
$uploadDir = '../../upload/';
if(!is_dir($uploadDir)){
mkdir($uploadDir, 0777);
}
$newPath = $uploadDir.iconv("utf-8", "cp949", $file->name);
if(file_put_contents($newPath, $file->content)) {
$sFileInfo .= "&bNewLine=true";
$sFileInfo .= "&sFileName=".$file->name;
$sFileInfo .= "&sFileURL=upload/".$file->name;
}
echo $sFileInfo;
}
?>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 506 B

@@ -1,390 +0,0 @@
/**
* Jindo Component
* @version 1.0.3
* NHN_Library:Jindo_Component-1.0.3;JavaScript Components for Jindo;
* @include Component, UIComponent, FileUploader
*/
jindo.Component = jindo.$Class({
_htEventHandler: null,
_htOption: null,
$init: function () {
var aInstance = this.constructor.getInstance();
aInstance.push(this);
this._htEventHandler = {};
this._htOption = {};
this._htOption._htSetter = {};
},
option: function (sName, vValue) {
switch (typeof sName) {
case "undefined":
return this._htOption;
case "string":
if (typeof vValue != "undefined") {
if (sName == "htCustomEventHandler") {
if (typeof this._htOption[sName] == "undefined") {
this.attach(vValue);
} else {
return this;
}
}
this._htOption[sName] = vValue;
if (typeof this._htOption._htSetter[sName] == "function") {
this._htOption._htSetter[sName](vValue);
}
} else {
return this._htOption[sName];
}
break;
case "object":
for (var sKey in sName) {
if (sKey == "htCustomEventHandler") {
if (typeof this._htOption[sKey] == "undefined") {
this.attach(sName[sKey]);
} else {
continue;
}
}
this._htOption[sKey] = sName[sKey];
if (typeof this._htOption._htSetter[sKey] == "function") {
this._htOption._htSetter[sKey](sName[sKey]);
}
}
break;
}
return this;
},
optionSetter: function (sName, fSetter) {
switch (typeof sName) {
case "undefined":
return this._htOption._htSetter;
case "string":
if (typeof fSetter != "undefined") {
this._htOption._htSetter[sName] = jindo.$Fn(fSetter, this).bind();
} else {
return this._htOption._htSetter[sName];
}
break;
case "object":
for (var sKey in sName) {
this._htOption._htSetter[sKey] = jindo.$Fn(sName[sKey], this).bind();
}
break;
}
return this;
},
fireEvent: function (sEvent, oEvent) {
oEvent = oEvent || {};
var fInlineHandler = this['on' + sEvent],
aHandlerList = this._htEventHandler[sEvent] || [],
bHasInlineHandler = typeof fInlineHandler == "function",
bHasHandlerList = aHandlerList.length > 0;
if (!bHasInlineHandler && !bHasHandlerList) {
return true;
}
aHandlerList = aHandlerList.concat();
oEvent.sType = sEvent;
if (typeof oEvent._aExtend == 'undefined') {
oEvent._aExtend = [];
oEvent.stop = function () {
if (oEvent._aExtend.length > 0) {
oEvent._aExtend[oEvent._aExtend.length - 1].bCanceled = true;
}
};
}
oEvent._aExtend.push({
sType: sEvent,
bCanceled: false
});
var aArg = [oEvent],
i, nLen;
for (i = 2, nLen = arguments.length; i < nLen; i++) {
aArg.push(arguments[i]);
}
if (bHasInlineHandler) {
fInlineHandler.apply(this, aArg);
}
if (bHasHandlerList) {
var fHandler;
for (i = 0, fHandler;
(fHandler = aHandlerList[i]); i++) {
fHandler.apply(this, aArg);
}
}
return !oEvent._aExtend.pop().bCanceled;
},
attach: function (sEvent, fHandlerToAttach) {
if (arguments.length == 1) {
jindo.$H(arguments[0]).forEach(jindo.$Fn(function (fHandler, sEvent) {
this.attach(sEvent, fHandler);
}, this).bind());
return this;
}
var aHandler = this._htEventHandler[sEvent];
if (typeof aHandler == 'undefined') {
aHandler = this._htEventHandler[sEvent] = [];
}
aHandler.push(fHandlerToAttach);
return this;
},
detach: function (sEvent, fHandlerToDetach) {
if (arguments.length == 1) {
jindo.$H(arguments[0]).forEach(jindo.$Fn(function (fHandler, sEvent) {
this.detach(sEvent, fHandler);
}, this).bind());
return this;
}
var aHandler = this._htEventHandler[sEvent];
if (aHandler) {
for (var i = 0, fHandler;
(fHandler = aHandler[i]); i++) {
if (fHandler === fHandlerToDetach) {
aHandler = aHandler.splice(i, 1);
break;
}
}
}
return this;
},
detachAll: function (sEvent) {
var aHandler = this._htEventHandler;
if (arguments.length) {
if (typeof aHandler[sEvent] == 'undefined') {
return this;
}
delete aHandler[sEvent];
return this;
}
for (var o in aHandler) {
delete aHandler[o];
}
return this;
}
});
jindo.Component.factory = function (aObject, htOption) {
var aReturn = [],
oInstance;
if (typeof htOption == "undefined") {
htOption = {};
}
for (var i = 0, el;
(el = aObject[i]); i++) {
oInstance = new this(el, htOption);
aReturn[aReturn.length] = oInstance;
}
return aReturn;
};
jindo.Component.getInstance = function () {
if (typeof this._aInstance == "undefined") {
this._aInstance = [];
}
return this._aInstance;
};
jindo.UIComponent = jindo.$Class({
$init: function () {
this._bIsActivating = false;
},
isActivating: function () {
return this._bIsActivating;
},
activate: function () {
if (this.isActivating()) {
return this;
}
this._bIsActivating = true;
if (arguments.length > 0) {
this._onActivate.apply(this, arguments);
} else {
this._onActivate();
}
return this;
},
deactivate: function () {
if (!this.isActivating()) {
return this;
}
this._bIsActivating = false;
if (arguments.length > 0) {
this._onDeactivate.apply(this, arguments);
} else {
this._onDeactivate();
}
return this;
}
}).extend(jindo.Component);
jindo.FileUploader = jindo.$Class({
_bIsActivating: false,
_aHiddenInput: [],
$init: function (elFileSelect, htOption) {
var htDefaultOption = {
sUrl: '',
sCallback: '',
htData: {},
sFiletype: '*',
sMsgNotAllowedExt: "업로드가 허용되지 않는 파일형식입니다",
bAutoUpload: false,
bAutoReset: true,
bActivateOnload: true
};
this.option(htDefaultOption);
this.option(htOption || {});
this._el = jindo.$(elFileSelect);
this._wel = jindo.$Element(this._el);
this._elForm = this._el.form;
this._aHiddenInput = [];
this.constructor._oCallback = {};
this._wfChange = jindo.$Fn(this._onFileSelectChange, this);
if (this.option("bActivateOnload")) {
this.activate();
}
},
_appendIframe: function () {
var sIframeName = 'tmpFrame_' + this._makeUniqueId();
this._welIframe = jindo.$Element(jindo.$('<iframe name="' + sIframeName + '" src="' + this.option("sCallback") + '?blank">')).css({
width: '10px',
border: '2px',
height: '10px',
left: '10px',
top: '10px'
});
document.body.appendChild(this._welIframe.$value());
},
_removeIframe: function () {
this._welIframe.leave();
},
getBaseElement: function () {
return this.getFileSelect();
},
getFileSelect: function () {
return this._el;
},
getFormElement: function () {
return this._elForm;
},
upload: function () {
this._appendIframe();
var elForm = this.getFormElement(),
welForm = jindo.$Element(elForm),
sIframeName = this._welIframe.attr("name"),
sFunctionName = sIframeName + '_func',
sAction = this.option("sUrl");
welForm.attr({
target: sIframeName,
action: sAction
});
this._aHiddenInput.push(this._createElement('input', {
'type': 'hidden',
'name': 'callback',
'value': this.option("sCallback")
}));
this._aHiddenInput.push(this._createElement('input', {
'type': 'hidden',
'name': 'callback_func',
'value': sFunctionName
}));
for (var k in this.option("htData")) {
this._aHiddenInput.push(this._createElement('input', {
'type': 'hidden',
'name': k,
'value': this.option("htData")[k]
}));
}
for (var i = 0; i < this._aHiddenInput.length; i++) {
elForm.appendChild(this._aHiddenInput[i]);
}
this.constructor._oCallback[sFunctionName + '_success'] = jindo.$Fn(function (oParameter) {
this.fireEvent("success", {
htResult: oParameter
});
delete this.constructor._oCallback[oParameter.callback_func + '_success'];
delete this.constructor._oCallback[oParameter.callback_func + '_error'];
for (var i = 0; i < this._aHiddenInput.length; i++) {
jindo.$Element(this._aHiddenInput[i]).leave();
}
this._aHiddenInput.length = 0;
this._removeIframe();
}, this).bind();
this.constructor._oCallback[sFunctionName + '_error'] = jindo.$Fn(function (oParameter) {
this.fireEvent("error", {
htResult: oParameter
});
delete this.constructor._oCallback[oParameter.callback_func + '_success'];
delete this.constructor._oCallback[oParameter.callback_func + '_error'];
for (var i = 0; i < this._aHiddenInput.length; i++) {
jindo.$Element(this._aHiddenInput[i]).leave();
}
this._aHiddenInput.length = 0;
this._removeIframe();
}, this).bind();
elForm.submit();
if (this.option("bAutoReset")) {
this.reset();
}
},
reset: function () {
var elWrapForm = jindo.$("<form>");
this._wel.wrap(elWrapForm);
elWrapForm.reset();
jindo.$Element(elWrapForm).replace(this._el);
var elForm = this.getFormElement(),
welForm = jindo.$Element(elForm);
welForm.attr({
target: this._sPrevTarget,
action: this._sAction
});
return this;
},
_onActivate: function () {
var elForm = this.getFormElement(),
welForm = jindo.$Element(elForm);
this._sPrevTarget = welForm.attr("target");
this._sAction = welForm.attr("action");
this._el.value = "";
this._wfChange.attach(this._el, "change");
},
_onDeactivate: function () {
this._wfChange.detach(this._el, "change");
},
_makeUniqueId: function () {
return new Date().getMilliseconds() + Math.floor(Math.random() * 100000);
},
_createElement: function (name, attributes) {
var el = jindo.$("<" + name + ">");
var wel = jindo.$Element(el);
for (var k in attributes) {
wel.attr(k, attributes[k]);
}
return el;
},
_checkExtension: function (sFile) {
var aType = this.option("sFiletype").split(';');
for (var i = 0, sType; i < aType.length; i++) {
sType = (aType[i] == "*.*") ? "*" : aType[i];
sType = sType.replace(/^\s+|\s+$/, '');
sType = sType.replace(/\./g, '\\.');
sType = sType.replace(/\*/g, '[^\\\/]+');
if ((new RegExp(sType + '$', 'gi')).test(sFile)) {
return true;
}
}
return false;
},
_onFileSelectChange: function (we) {
var sValue = we.element.value,
bAllowed = this._checkExtension(sValue),
htParam = {
sValue: sValue,
bAllowed: bAllowed,
sMsgNotAllowedExt: this.option("sMsgNotAllowedExt")
};
if (sValue.length && this.fireEvent("select", htParam)) {
if (bAllowed) {
if (this.option("bAutoUpload")) {
this.upload();
}
} else {
alert(htParam.sMsgNotAllowedExt);
}
}
}
}).extend(jindo.UIComponent);
File diff suppressed because it is too large Load Diff
@@ -1,101 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="Content-Script-Type" content="text/javascript">
<meta http-equiv="Content-Style-Type" content="text/css">
<title>사진 첨부하기 :: SmartEditor2</title>
<style type="text/css">
/* NHN Web Standard 1Team JJS 120106 */
/* Common */
body,p,h1,h2,h3,h4,h5,h6,ul,ol,li,dl,dt,dd,table,th,td,form,fieldset,legend,input,textarea,button,select{margin:0;padding:0}
body,input,textarea,select,button,table{font-family:'돋움',Dotum,Helvetica,sans-serif;font-size:12px}
img,fieldset{border:0}
ul,ol{list-style:none}
em,address{font-style:normal}
a{text-decoration:none}
a:hover,a:active,a:focus{text-decoration:underline}
/* Contents */
.blind{visibility:hidden;position:absolute;line-height:0}
#pop_wrap{width:383px}
#pop_header{height:26px;padding:14px 0 0 20px;border-bottom:1px solid #ededeb;background:#f4f4f3}
.pop_container{padding:11px 20px 0}
#pop_footer{margin:21px 20px 0;padding:10px 0 16px;border-top:1px solid #e5e5e5;text-align:center}
h1{color:#333;font-size:14px;letter-spacing:-1px}
.btn_area{word-spacing:2px}
.pop_container .drag_area{overflow:hidden;overflow-y:auto;position:relative;width:341px;height:129px;margin-top:4px;border:1px solid #eceff2}
.pop_container .drag_area .bg{display:block;position:absolute;top:0;left:0;width:341px;height:129px;background:#fdfdfd url(./img/bg_drag_image.png) 0 0 no-repeat}
.pop_container .nobg{background:none}
.pop_container .bar{color:#e0e0e0}
.pop_container .lst_type li{overflow:hidden;position:relative;padding:7px 0 6px 8px;border-bottom:1px solid #f4f4f4;vertical-align:top}
.pop_container :root .lst_type li{padding:6px 0 5px 8px}
.pop_container .lst_type li span{float:left;color:#222}
.pop_container .lst_type li em{float:right;margin-top:1px;padding-right:22px;color:#a1a1a1;font-size:11px}
.pop_container .lst_type li a{position:absolute;top:6px;right:5px}
.pop_container .dsc{margin-top:6px;color:#666;line-height:18px}
.pop_container .dsc_v1{margin-top:12px}
.pop_container .dsc em{color:#13b72a}
.pop_container2{padding:46px 60px 20px}
.pop_container2 .dsc{margin-top:6px;color:#666;line-height:18px}
.pop_container2 .dsc strong{color:#13b72a}
.upload{margin:0 4px 0 0;_margin:0;padding:6px 0 4px 6px;border:solid 1px #d5d5d5;color:#a1a1a1;font-size:12px;border-right-color:#efefef;border-bottom-color:#efefef;length:300px;}
:root .upload{padding:6px 0 2px 6px;}
</style>
</head>
<body>
<div id="pop_wrap">
<!-- header -->
<div id="pop_header">
<h1>사진 첨부하기</h1>
</div>
<!-- //header -->
<!-- container -->
<!-- [D] HTML5인 경우 pop_container 클래스와 하위 HTML 적용
그밖의 경우 pop_container2 클래스와 하위 HTML 적용 -->
<div id="pop_container2" class="pop_container2">
<!-- content -->
<!-- <form id="editor_upimage" name="editor_upimage" action="FileUploader.php" method="post" enctype="multipart/form-data" onSubmit="return false;"> -->
<form id="editor_upimage" name="editor_upimage" method="post" enctype="multipart/form-data" onSubmit="return false;">
<div id="pop_content2">
<input type="file" class="upload" id="uploadInputBox" name="Filedata">
<p class="dsc" id="info"><strong>10MB</strong>이하의 이미지 파일만 등록할 수 있습니다.<br>(JPG, GIF, PNG, BMP)</p>
</div>
</form>
<!-- //content -->
</div>
<div id="pop_container" class="pop_container" style="display:none;">
<!-- content -->
<div id="pop_content">
<p class="dsc"><em id="imageCountTxt">0장</em>/10장 <span class="bar">|</span> <em id="totalSizeTxt">0MB</em>/50MB</p>
<!-- [D] 첨부 이미지 여부에 따른 Class 변화
첨부 이미지가 있는 경우 : em에 "bg" 클래스 적용 //첨부 이미지가 없는 경우 : em에 "nobg" 클래스 적용 -->
<div class="drag_area" id="drag_area">
<ul class="lst_type" >
</ul>
<em class="blind">마우스로 드래그해서 이미지를 추가해주세요.</em><span id="guide_text" class="bg"></span>
</div>
<div style="display:none;" id="divImageList"></div>
<p class="dsc dsc_v1"><em>한 장당 10MB, 1회에 50MB까지, 10개</em>의 이미지 파일을<br>등록할 수 있습니다. (JPG, GIF, PNG, BMP)</p>
</div>
<!-- //content -->
</div>
<!-- //container -->
<!-- footer -->
<div id="pop_footer">
<div class="btn_area">
<a href="#"><img src="./img/btn_confirm.png" width="49" height="28" alt="확인" id="btn_confirm"></a>
<a href="#"><img src="./img/btn_cancel.png" width="48" height="28" alt="취소" id="btn_cancel"></a>
</div>
</div>
<!-- //footer -->
</div>
<script type="text/javascript" src="jindo.min.js" charset="utf-8"></script>
<script type="text/javascript" src="jindo.fileuploader.js" charset="utf-8"></script>
<script type="text/javascript" src="attach_photo.js" charset="utf-8"></script>
</body>
</html>
@@ -1,8 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>Smart Editor&#8482; WYSIWYG Mode</title>
</head>
<body class="se2_inputarea" style="height:0;-webkit-nbsp-mode:normal"></body>
</html>
@@ -1,9 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7">
<title>Smart Editor&#8482; WYSIWYG Mode</title>
</head>
<body class="se2_inputarea" style="height:0"></body>
</html>
Binary file not shown.
Binary file not shown.
Binary file not shown.
-56
View File
@@ -1,56 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:task="http://www.springframework.org/schema/task"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-3.0.xsd
">
<context:component-scan base-package="com.pms"></context:component-scan>
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
<value>classpath:com/pms/message/message</value>
<value>classpath:com/pms/message/common</value>
</list>
</property>
<property name="cacheSeconds">
<value>60</value>
</property>
<property name="defaultEncoding" value="UTF-8"/>
</bean>
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
<property name="defaultLocale" value="ko"/>
</bean>
<bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="lang" />
</bean>
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/view"/>
<property name="suffix" value=".jsp"/>
<property name="order" value="1"/>
</bean>
<bean id="jacksonMessageConverter" class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"></bean>
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="messageConverters">
<list>
<ref bean="jacksonMessageConverter"/>
</list>
</property>
</bean>
<task:scheduler id="gsScheduler" pool-size="10" />
<task:annotation-driven scheduler="gsScheduler" />
</beans>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More