모든 사례

코지셀

패션 플랫폼 위탁판매 셀러의 주문을 받아 공급처 구매와 결제까지 자동으로 이어 주는 구독형 백오피스 솔루션

기간
2026년 2월부터
역할
기획, 개발, 구독 결제, 배포, 운영 (1인)
커밋
3,425개
제공 방식
고객 전용 서버, 또는 고객 서버에 설치
현재 버전
1.4.307

쓴 기술 Django 5, DRF, Celery, PostgreSQL, Redis, Next.js 16, Chrome 확장(MV3), Windows 에이전트(Python), FastAPI, AWS ECR, EC2, Lightsail

코지셀 주문 브랜드 리포트 화면. 주문 수, 총매출, 취소, 반품, 순매출 카드와 브랜드별 매출 표가 있다.
주문 브랜드 리포트. 테스트 데이터이고, 브랜드명과 연동 도구 이름은 흐리게 처리했어요

문제

위탁판매 셀러는 국내 대형 패션 플랫폼(이하 공급처)의 상품을 오픈마켓 6곳에 올려 팔고, 주문이 들어오면 공급처에서 같은 상품을 사서 고객에게 바로 보내요. 주문 수집은 이미 자동이었지만, 공급처에서 구매하고 그 결과를 주문에 적고 송장을 넣는 일은 사람이 한 건씩 했어요.

사업자가 여러 개면 일이 더 꼬였어요. 주문이 들어온 쇼핑몰과 사업자마다 공급처 계정과 결제수단을 따로 써야 했거든요. 쓰던 상품 관리 도구는 주문을 모아 주기만 하고, 공급처에서 사는 일은 하지 않았어요.

“주문이 들어온 쇼핑몰/사업자 그룹별로 [공급처] 계정과 결제수단을 분리해야 한다”
자동 주문 설계 문서

목표

  • 고객 데이터가 절대 섞이지 않게, 고객마다 서버를 따로 둔다.
  • 같은 주문을 두 번 결제하지 않는다. 한 후보는 한 번만 결제하고, 같은 계정으로 동시에 결제하지 않는다.
  • 결제가 진행 중일 때는 업데이트하지 않는다. 멈추고, 진행 중인 결제가 0건이 되면 업데이트하고, 다시 켠다.
  • 새 버전은 운영자가 승인한 뒤에만, 카나리 고객부터 나간다.

풀어 간 과정

처음 시도한 방법, 막힌 지점, 방향을 바꾼 이유를 시간 순서로 적었어요.

  1. 2026년 2월

    상품 수집부터 직접 만들었어요

    쓰던 상품 관리 도구의 확장을 뜯어보니, 상품 페이지 HTML을 통째로 서버에 보내 서버에서 파싱하고 있었어요. 한 번에 수백 KB에서 수 MB였고, 특정 쇼핑몰 솔루션에 묶여 있었어요. 마켓 API를 먼저 붙이고, 확장에서 파싱해 수 KB짜리 데이터만 보내는 확장을 새로 만들었어요.

  2. 2026년 3~4월

    구매는 사람이, 알림은 서버가

    공급처 구매는 아직 운영자가 직접 했어요. 대신 서버가 결제 완료 주문을 모아 매시간 텔레그램으로 알리게 했어요. 확장이 주문을 확인하게 하는 방법도 있었지만, 일정과 재시도 상태는 서버가 가져야 해서 뺐어요.

  3. 2026년 5월

    결제 직전까지만 자동으로

    첫 자동 구매는 결제 직전 확인 화면에서 멈추고, 실제 결제는 텔레그램에서 승인해야 넘어갔어요. 기존 워커를 고쳐 쓰지 않고 구매 전용 워커를 새로 만들었어요. 결제 키패드를 읽는 방식 하나는 써 본 뒤 되돌렸어요.

  4. 2026년 5~6월

    사람이 누르던 구매를 자동 체인으로

    사업자 그룹마다 공급처 계정과 결제수단을 나누는 설계를 세우고, 수동으로 시작한 구매도 같은 체인을 타게 했어요. 35시간 된 정상 주문이 후보에서 빠지는 걸 보고 후보 유효기간을 2시간에서 48시간으로 늘렸어요. 마진 조건을 만족하면 승인이 자동으로 넘어가게 했고, 중복 결제 가드와 확인되지 않은 결제 차단을 넣었어요.

  5. 2026년 5~7월

    고객마다 서버, 업데이트는 하트비트로

    처음에는 한 서버에 여러 고객을 두는 구조를 가정했지만, 5월 말 고객마다 서버를 두는 쪽으로 바꾸고 라이선스 시스템을 만들었어요. 라이선스가 없으면 부팅을 막았더니 만료 뒤 재시작한 서버가 갱신 화면에도 못 들어가서, 잠긴 상태로라도 켜지게 고쳤어요. 업데이트는 SSH 수작업 계획에서, 고객 서버가 하트비트로 새 버전을 확인하고 운영자가 승인한 카나리부터 받는 방식으로 바꿨어요. 설치형 고객 서버는 스스로 올리지 않고, 고객이 앱에서 승인해야 업데이트돼요.

지금의 구조

주문이 들어와 결제되기까지, 새 버전이 고객 서버에 깔리기까지의 순서예요.

  1. 1

    크롬 확장이 공급처 상품 정보를 모아 약 5KB짜리 데이터로 서버에 보내요.

  2. 2

    6개 마켓 주문이 모이는 상품 관리 도구에서 Celery가 새 주문을 주기적으로 가져와 구매 후보로 저장해요.

  3. 3

    자동 주문 체인이 클레임 여부, 정산금, 최근 실행 기록, 락을 확인해 조건에 맞는 주문만 골라요. 한 번에 최대 5건, 90초 간격이에요.

  4. 4

    워커가 공급처 주문서를 쓰고 간편결제로 결제해요. 결제 계정마다 한 번에 한 건만 진행해요.

  5. 5

    결과가 불확실하면 '차단'으로 멈추고 텔레그램으로 알려요. 확인은 다시 결제하지 않고 주문 내역 대조로 끝내요.

  6. 6

    고객 서버는 라이선스 서버에 하트비트를 보내 새 버전을 확인해요. 진행 중인 결제가 0건이 되면 이미지를 받아 띄우고, 헬스체크에 실패하면 이전 버전으로 돌아가요.

새 버전이 고객에게 가는 순서

  1. 운영 서버에 먼저 배포
  2. digest를 고정해 릴리스 등록
  3. 운영자 승인 후 카나리 고객에게
  4. 확인하고 전체 고객으로

고객 서버 안에서 일어나는 일

  1. 하트비트로 새 버전 확인
  2. 결제를 멈추고 진행 중 0건까지 대기
  3. 이미지 받아 띄우고 마이그레이션
  4. 헬스체크

헬스체크에 실패하면 DB는 두고 이미지만 이전 버전으로 되돌려요

코지셀 배포 흐름 도식
코지셀 자동 처리 작업함. 자동 실패 1건, 자동 보류 1건, 상태 미확인 1건이 사용자 확인을 기다린다는 안내가 있다.
자동 처리 작업함. 자동화가 확신하지 못한 건을 모아 사람에게 넘겨요. 상태 미확인은 실패로도 성공으로도 처리하지 않아요

핵심 결정

고객마다 서버를 따로 두기

이유
한 서버에 여러 고객을 두면 고객 사이에 데이터가 섞일 위험을 코드로 계속 막아야 해요. 서버를 나누면 그 위험이 거의 사라지고, 암호화 키도 서버마다 따로 둘 수 있어요.
다른 선택지
한 서버에 여러 고객을 두는 멀티테넌트 구조
감수한 것
서버를 고객 수만큼 운영해야 해요. 그래서 라이선스 서버, 원격 업데이트, 카나리 배포를 직접 만들어야 했어요.

확신이 없으면 실패가 아니라 '차단'

이유
실패로 처리하고 재시도하면 같은 주문이 다시 구매 대상으로 뽑혀 이중결제가 날 수 있어요. 실제로 겪은 일이라, 상태를 다시 분류하고 결제는 막는 쪽을 기본으로 정했어요.
다른 선택지
실패로 기록하고 자동 재시도
감수한 것
실패처럼 보이는 알림이 먼저 가서 헷갈릴 수 있고, 확인될 때까지 그 계정의 자동 구매가 멈춰요.

릴리스를 증거로 묶기

이유
고객 서버에 무엇이 깔리는지 정확히 알아야 문제가 생겼을 때 되돌릴 수 있어요. 이미지 3개의 digest가 정확히 일치해야 하고, 릴리스 단계는 계획, 진행, 완료 순서로만 넘어가며 단계마다 Ed25519로 서명한 기록이 남아요. 카나리 대상은 롤아웃 ID의 해시로 고정해요.
다른 선택지
고객 서버가 최신 버전을 알아서 받는 자동 업데이트, 또는 쿠버네티스 도입
감수한 것
절차가 무거워졌어요. 기록 순서를 틀려 릴리스를 다시 발행한 적도 있어요.

어려웠던 문제

겪었거나 배포 전에 막은 문제를 어떻게 알아챘고, 무엇을 의심해 원인을 찾았는지, 다시 일어나지 않게 무엇을 바꿨는지 기록했어요.

운영자가 이미 산 주문을 한 번 더 결제한 문제

운영자가 공급처에서 직접 산 주문을 자동 구매가 다시 결제해, 같은 상품이 두 번 결제됐어요. 중복 결제를 막는 가드를 넣은 지 닷새 만이었어요.

왜 어려웠나

사람이 공급처에서 직접 주문하면 자동 구매 실행 기록이 남지 않아서, DB만 보는 중복 방지로는 잡을 수 없었어요. 공급처 주문 내역을 조회하는 가드는 있었지만 재시도일 때만 켜지게 만들어 두었어요. 첫 시도는 안전하다고 본 거예요. 게다가 상품 관리 도구 쪽 상태가 바뀌어도 동기화가 그 행을 다시 읽지 않아서, 이미 처리된 주문이 계속 구매 대상으로 남아 있었어요.

추적 과정

  1. 1. 매칭 기준이 틀렸을까?

    가드 첫 부분을 보니, 첫 시도면 공급처 주문 내역을 아예 조회하지 않고 '중복 없음'을 돌려주고 있었어요.

  2. 2. 이미 처리된 주문이 왜 아직 후보일까?

    상품 관리 도구의 상태가 바뀌면 동기화가 그 행을 다시 읽지 않아서, 로컬 행이 예전 상태로 굳어 있었어요.

  3. 3. 조건문만 지우면 될까?

    아니었어요. 첫 시도에서는 조회 시작 시각이 실행 기록의 생성 시각으로 잡혀서, 그 전에 사람이 직접 한 주문이 조회 범위 밖으로 빠졌어요.

  4. 4. 어느 계정으로 조회해야 할까?

    첫 시도에는 계정 선택 기록이 없을 수 있어서, 주문 라우팅에 지정된 공급처 계정을 대신 쓰게 했어요.

원인

중복 가드(evaluate_run_for_duplicate)가 재시도가 아니면 공급처를 조회하지 않고 바로 '중복 없음'을 돌려줬어요.

운영자자동 구매
  1. 1고객 주문이 구매 후보가 됨
  2. 2공급처에서 직접 구매 (실행 기록 없음)
  3. 3상품 관리 도구 상태가 바뀜동기화가 행을 다시 읽지 않아 후보로 남음
  4. 4후보를 첫 시도로 실행
  5. 5첫 시도라서 공급처 조회를 건너뜀
  6. 6결제, 같은 상품 두 번 결제

해결

첫 시도에도 공급처 주문 내역을 조회해요. 조회 시작 시각은 고객이 주문한 시각으로 잡고, 계정 선택 기록이 없으면 라우팅에 지정된 계정을 써요. 상품번호, 옵션, 수량과 금액이 확실히 일치할 때만 결제를 막고 후보에서 빼요. 공급처 조회 자체가 실패했을 때만은 모든 주문이 멈추지 않도록 통과시키는 예외를 그대로 뒀어요. 결제 결과가 불확실할 때 차단하는 것과는 다른 단계의 판단이에요.

고치기 전

if not _is_redispatch(run):
    return DuplicateMatch("none", reason="first_attempt")
since = _first_attempt_at(run)

고친 뒤

is_redispatch = _is_redispatch(run)
account_ids = _supplier_account_ids_for_order(run)
if not account_ids:
    fb = _dispatch_supplier_account_id(run)  # 라우팅에 지정된 계정
    if fb:
        account_ids = [fb]
since = _first_attempt_at(run) if is_redispatch else _candidate_order_datetime(run)
실제 수정 diff를 줄이고 공급처 이름이 들어간 식별자는 일반화했어요. backend/order_process/services/ 의 중복 가드

검증

  • 새 테스트 4개를 포함해 중복 가드 테스트 30개 통과 (사람이 먼저 산 주문 감지, 조회 실패 시 통과, 일치 주문 없음, 계정 없음)
  • 파이프라인 통합 테스트 63개 통과
  • 독립 리뷰 승인 뒤 같은 날 1.0.38로 배포

재발 방지

  • 수취인이나 옵션이 다르면 중복으로 보지 않도록 오탐을 보정했어요.
  • 공급처 주문 목록이 잘려서 오면 '불완전'으로 처리해요.
  • '중복 주문 감지'는 처리량을 늘리는 설계에서도 약화하면 안 되는 불변식으로 설계 문서에 적었어요.

남긴 원칙

중복 확인은 재시도뿐 아니라 첫 시도에도 돌려야 해요. 사람은 시스템 밖에서 먼저 움직이니까요.

업데이트 버튼이 자기 자신을 종료시키던 문제

설치형 고객이 1.0.5부터 1.0.7까지 앱 안에서 업데이트를 받지 못했어요. 버튼을 누르면 502와 함께 '이전 버전으로 복구됐다'는 문구가 떴지만 실제로는 아무것도 바뀌지 않았어요. 남은 방법은 운영자가 SSH로 들어가 직접 업데이트하는 것뿐이었어요.

왜 어려웠나

버그가 두 겹이었어요. 백엔드 컨테이너에는 docker를 다룰 도구와 소켓이 없었고, 그걸 붙여도 업데이트가 백엔드까지 다시 만들면서 업데이트하던 프로세스가 같이 종료됐어요. 업데이터가 자기가 돌고 있는 컨테이너를 교체하는 구조였던 거예요. 결제 세션을 들고 있는 워커를 그냥 다시 만들면 진행 중인 결제도 끊겼고요.

추적 과정

  1. 1. 502는 어디서 날까?

    업데이트 API가 요청 안에서 업데이트를 동기로 실행하고 있었고, 첫 이미지 pull에서 바로 실패했어요.

  2. 2. docker 소켓만 붙이면 될까?

    아니었어요. 컨테이너를 다시 만드는 순간 업데이터가 같이 죽어서 헬스체크와 롤백까지 가지 못했어요. 업데이트 로직은 멀쩡하고 실행 위치만 틀렸다고 결론 냈어요.

  3. 3. 그럼 어디서 실행해야 할까?

    호스트의 systemd 감시자가 요청 파일을 보고, compose 프로젝트 밖의 일회성 컨테이너를 띄우게 했어요. bash로 다시 짜는 안은 검증과 롤백 로직이 중복돼서 버렸어요.

  4. 4. 실제 서버에서도 돌아갈까?

    아무것도 바꾸지 않는 스모크를 돌리자, systemd 환경에 $HOME이 없어 스크립트가 바로 끝나고 작업 디렉터리 옵션이 이미지 설정을 덮어쓰는 문제가 나왔어요.

  5. 5. 첫 실제 업데이트는?

    헬스체크 요청이 허용되지 않은 호스트 이름으로 400을 받고, 파일 권한 때문에 설정을 읽거나 쓰지 못했어요. 매번 자동 롤백이 서버를 지켰고, 하나씩 고쳐 1.0.9에서 1.0.10으로 업데이트에 성공했어요.

원인

업데이트 API(FleetApplyUpdateView)가 요청을 처리하면서, 교체 대상인 백엔드 컨테이너 안에서 업데이트를 동기로 실행했어요.

업데이터 (백엔드 안)docker compose
  1. 1버튼 클릭, 요청 안에서 바로 실행
  2. 2docker 도구가 없어 pull 실패, 502와 '복구됨' 응답
  3. 3(소켓이 있었다면) pull과 digest 검증 통과
  4. 4컨테이너 교체 명령 실행태그가 바뀌어 백엔드까지 다시 만듦
  5. 5백엔드와 함께 업데이터도 종료새 백엔드 기동
  6. 6헬스체크, 롤백, 결과 보고 모두 실행 안 됨

해결

API는 업데이트를 직접 하지 않고 요청 파일만 남긴 뒤 202를 돌려줘요. 이미 요청이 있으면 409예요. 호스트의 systemd 감시자가 이 파일을 보고 스크립트를 실행해요. 스크립트는 잠금을 잡고, 자동 구매를 멈추고, 진행 중인 결제가 0건이 될 때까지 기다린 뒤, 현재 버전 이미지로 일회성 업데이트 컨테이너를 띄워요. 이 컨테이너는 compose 프로젝트 밖에 있어서 다른 컨테이너가 다시 만들어져도 살아남아요.

고치기 전

try:
    result = run_fleet_update(str(offer["target_version"]), ...)
except FleetUpdateError:
    return Response({"detail": "...이전 버전으로 복구됨"}, status=502)

고친 뒤

# API: 요청만 남기고 바로 응답
if self_update_request_pending():
    return Response({...}, status=409)
if not request_self_update(offer):   # .fleet-control/request.json
    return Response({...}, status=501)
return Response({"queued": True}, status=202)

# 호스트 스크립트 (systemd path 감시자가 실행, 줄인 것)
flock -n 9 || exit 0; trap cleanup EXIT INT TERM
pause; drain until inflight == 0        # 15분 넘으면 업데이트 중단
docker run --rm -v /var/run/docker.sock:... <현재 버전 이미지> \
  python manage.py fleet_update --request request.json
실제 코드를 줄인 것이에요. backend/licensing/fleet_views.py, scripts/fleet-apply.sh

검증

  • fleet 테스트 80개 통과, 새 테스트 17개 (요청 대기열, 미설치 시 501, 중복 요청 409, 오래된 요청 무효 등)
  • 코드 리뷰에서 나온 HIGH 2건 반영
  • 실제 서버에서 아무것도 바꾸지 않는 스모크, 이어서 1.0.9에서 1.0.10 실제 업데이트

재발 방지

  • 요청 파일은 1시간이 지나면 무효이고, 동시에 하나만 실행돼요.
  • 15분 안에 진행 중인 결제가 0건이 되지 않으면 업데이트하지 않아요.
  • 이미 설치된 고객을 위한 보강 스크립트를 만들고, 새 설치에는 허용 호스트 설정을 자동으로 넣어요.

남긴 원칙

'복구를 보장한다'는 주석은 보장이 아니에요. 새로 만든 자체 업데이트 장치는 처음 한 번은 예전 방식으로 설치해야 해요.

배포 강제 종료가 남긴 브라우저 프로필 잠금

공급처 작업을 돌리는 계정 하나가 11일 동안 매일 실패했어요. 브라우저가 뜨기도 전에 0.006초 만에 끝났고, 최근 실패 10건이 모두 '브라우저 실행 실패'였어요.

왜 어려웠나

Chromium의 프로필 잠금은 '호스트이름-PID' 형식인데, 컨테이너에서는 호스트이름이 컨테이너 ID예요. 컨테이너가 새로 만들어지면 남은 잠금이 '다른 컴퓨터가 쓰는 프로필'로 보여요. 앱의 정리 로직은 호스트이름이 다르면 손대지 않게 되어 있었고, 다른 PID 네임스페이스라 그 프로세스가 살아 있는지 확인할 방법도 없었어요. 강제 종료 순간 브라우저가 열려 있을 때만 생기는 간헐적인 문제였고요.

추적 과정

  1. 1. 잠금 주인이 아직 살아 있을까?

    잠금이 가리키는 호스트이름은 어떤 컨테이너에도 없었고, 그 프로필을 쓰는 Chromium 프로세스도 0개였어요.

  2. 2. 정리 코드가 안 불렸을까?

    정상으로 불렸어요. 다른 호스트의 잠금은 설계상 회수하지 않는 거였어요.

  3. 3. 오프라인 복구 도구로 충분할까?

    첫 수정은 같은 볼륨을 쓰는 워커 5개를 멈춰야 해서, 운영 중에는 실행할 수 없었어요.

  4. 4. 잠금은 언제 생겼을까?

    잠금 파일 시각을 같은 날 배포 로그와 맞춰 보니, 14분 뒤에 '2분 안에 종료되지 않아 강제 종료'한 기록이 있었어요.

  5. 5. 실패가 모두 같은 원인일까?

    정확히 30초에 끝난 실패는 페이지 로딩 대기 시간 초과로 원인이 달랐어요. 따로 분류했어요.

원인

배포 때 컨테이너가 2분 종료 유예를 넘겨 강제 종료됐고, Chromium이 잠금을 풀지 못했어요. 사라진 컨테이너를 가리키는 잠금이 남았는데 회수할 경로가 없었어요. 같은 서버에서 60일 동안 배포 중 강제 종료가 40번 있었어요.

배포Chromium
  1. 19월 16일 컨테이너 생성
  2. 29월 17일 09:53 프로필 잠금 생성
  3. 310:07 종료 신호, 2분을 넘겨 강제 종료잠금을 풀지 못함
  4. 4새 컨테이너, 다른 호스트이름
  5. 59월 18일부터 매일 실행 거절
  6. 69월 28일 온라인 미리보기 후 적용워커를 멈추지 않고 복구

해결

잠금의 호스트이름이 컨테이너 ID 형식이면, 그 ID를 가진 컨테이너가 상태와 상관없이 하나라도 남아 있는지 확인해요. 이어서 호스트의 모든 프로세스를 훑어, 장치와 inode가 같은 프로필 디렉터리를 쓰는 Chromium이 있는지 봐요. 확인이 하나라도 불완전하면 지우지 않아요. 워커를 멈추지 않는 온라인 복구 모드와, 배포가 끝날 때마다 자동으로 회수하는 단계도 넣었어요.

고치기 전

host, pid_text = os.readlink(lock_path).rsplit("-", 1)
if host != socket.gethostname() or not pid_text.isdigit():
    return False   # 컨테이너가 바뀌면 영원히 False

고친 뒤

m = re.fullmatch(r"([a-f0-9]{12}|[a-f0-9]{64})-([1-9][0-9]*)", target)
for c in containers:                     # 모든 상태의 컨테이너
    if c["Id"].startswith(m[1]) or c["Config"].get("Hostname") == m[1]:
        raise ProfileInUseError("former_owner_container_present")
ident = (info.st_dev, info.st_ino)
for ref in process_refs:                 # /proc/<pid>/root 기준 --user-data-dir
    if ref.error:
        raise ProfileInUseError("host_process_scan_incomplete")
    if ref.identity == ident:
        raise ProfileInUseError("host_profile_process_still_active")
실제 코드를 줄인 것이에요. browser_manager.cleanup_stale_locks에서 profile_lock.inspect_orphaned_container_lock으로

검증

  • 새 테스트 14개, 관련 테스트 109개 통과
  • 실제 Chromium 컨테이너로 강제 종료를 재현해, 주인이 살아 있거나 일시정지된 잠금은 회수하지 않는 것을 확인
  • 수정을 되돌리면 회귀 테스트가 실패하는 것까지 확인한 뒤, 운영 서버에서 미리보기 후 적용

재발 방지

  • 배포가 끝날 때마다 남은 잠금을 자동으로 회수해요. 회수에 실패해도 배포는 멈추지 않고 경고만 남겨요.
  • 브라우저 실행 실패를 원인별로 나눠 기록해요.
  • 고객 서버 업데이트 경로에도 같은 회수를 넣는 일이 남아 있어요.

남긴 원칙

프로필 잠금 장치가 풀려 있다고 Chromium이 죽었다는 증거는 아니에요. 자식 프로세스는 부모보다 오래 살 수 있어서, 지우기 전에 프로세스를 직접 확인해요.

결과

운영 서버 배포
38분 48초에서 5분 58초로
고객용 이미지 발행
10분 36초에서 4분 12초로
고객 서버 업데이트
결제 일시정지부터 재개까지 2분 58초
릴리스
60일 동안 241번 (2026년 6월부터 8월 초)
Windows 에이전트 재빌드
약 80% 감소
라이선스 서버 테스트
테스트 함수 598개
  • 운영자가 공급처 구매와 결제를 한 건씩 하지 않아요. 자동 체인이 조건에 맞는 주문을 골라 결제하고, 확신하지 못한 건만 작업함에 남겨 운영자는 그것만 확인해요.
  • 고객 서버 업데이트가 SSH 수작업에서 앱 안의 버튼 하나로 바뀌었어요.
  • 고객 PC의 브라우저가 닫혀 있어도 관제 화면에서 PC 상태를 볼 수 있어요.

배운 점

  • 느린 빌드를 돌리기 전에, 실패할 조건부터 먼저 확인해요.
  • 한 번 빌드한 산출물을 그대로 승격해요. 환경마다 다시 빌드하지 않아요.
  • 명령이 끝났다는 것과 일이 성공했다는 것은 달라요. 업무 결과로 확인해요.
코지셀 서비스 소개 페이지 첫 화면. 복잡한 멀티몰 관리, 이제 편안하게라는 제목이 있다.
서비스 소개 페이지 cozysell.ewr.kr

다음 사례

로렌츠 통합 운영 플랫폼

직원들은 주문을 하나씩 복사해 등록하고, 배송은 택배사 사이트에서 직접 확인했어요.