포트폴리오
프로젝트 목록
스마트스토어 고객이 반쪽짜리로 들어왔다 CRM 콘솔에 카페24 자사몰 데이터를 먼저 붙이고 나니, 다음 요청은 네이버 스마트스토어였다. MotherK는 자사몰과 함께 스마트스토어에서도 판매하고 있었는데, 그쪽 고객은 CRM에서 전혀 보이지 않았다. 스마트스토어에서 두 번 산 고객이 자사몰에서 한 번 더 사도, 시스템은 이 사람을 "신규 1회 구매자"로 보고 있었다. 네이버 커머스 API를 연결해 주문을 받아 오는 것까지는 순서대로 진행됐다. 손이 많이 간 건 그다음이었다. API가 일정 기간이 지난 주문의 구매자 정보를 마스킹해서 돌려줬고, 그 때문에 한 사람이 여러 명으로 쪼개졌다. 이 글은 네이버 주문 수집 구조와, 마스킹된 고객을 다섯 단계에 걸쳐 정리한 과정을 다룬다. 조건: 24시간, 300건, 그리고 마스킹 네이버 커머스 API에는 지켜야 할 제약이 몇 가지 있었다. 인증 : client_id 와 타임스탬프를 이어 붙인 문자열을 client secret을 salt로 bcrypt 해시하고, Base64로 인코딩한 서명으로 OAuth2 토큰을 받는다. 조회 범위 : 변경 주문 조회( last-changed-statuses )는 한 번에 최대 24시간 범위만 받는다. 90일치를 가져오려면 90번 호출해야 한다. 상세 조회 : 주문 상세는 한 번에 300건까지 조회된다. 개인정보 마스킹 : 시간이 지난 주문은 주문자 이름과 전화번호 일부가 * 로 가려져 온다. 예를 들어 이름은 "홍*동", 전화번호는 "0101***2***" 같은 형태다. 고객을 구분할 회원 ID가 없었기 때문에 "이름_전화번호"를 고객 키로 썼다. 이 선택이 마스킹 문제를 그대로 드러냈다. 같은 사람이라도 최근 주문에서는 실제 이름·번호로, 오래된 주문에서는 마스킹된 값으로 들어와 키가 두 개가 됐다. 수집 구조 수집은 NaverCommerceAPI 클래스 하나에 모았다. 토큰은 만료 60초 전까지 재사용하고, 기간 조회는 24시간 단위로 잘라 돌면서 300건씩 상세를 가져온다. 진행률 콜백을 받아서 수동 동기화를 실행하면 화면 위젯에 "주문 수집 중... 15/90일 (230건)"처럼 표시되게 했다. 90일치를 받는 데 시간이 꽤 걸려서, 진행률이 보이지 않으면 사용자가 멈춘 줄 알고 버튼을 다시 누를 수 있어서다. while current < end_date: chunk_end = min(current + timedelta(hours=24), end_date) from_str = current.strftime("%Y-%m-%dT%H:%M:%S.000+09:00") to_str = chunk_end.strftime("%Y-%m-%dT%H:%M:%S.000+09:00") changed = self.get_changed_orders(from_str, to_str) if changed: order_ids = [item["productOrderId"] for item in changed] for i in range(0, len(order_ids), 300): # 상세는 300건씩 details = self.get_order_details(order_ids[i:i + 300]) all_orders.extend(details) current = chunk_end day_idx += 1 if on_progress: on_progress(day_idx, total_days, len(all_orders)) 자동 동기화는 매일 오전 6시에 최근 3일치만 다시 가져온다. 하루치만 받지 않는 이유는 주문 상태가 며칠 사이에 바뀌고, 전날 동기화가 실패했을 때 다음 날 자동으로 메워지게 하기 위해서다. 동기화가 끝나면 세그먼트 분류와 마스킹 복원이 이어서 실행된다. 세그먼트는 고객을 한 명씩 돌며 계산하지 않고, UPDATE ... SET segment = CASE ... SQL 한 번으로 전체를 갱신했다. 시행착오 1. 마스킹 고객을 다섯 단계로 정리하기 90일치를 처음 동기화하고 고객 목록을 열어 보니, 별표가 섞인 이름과 번호가 많았다. 이 고객들에게는 문자를 보낼 수 없었고, 같은 사람의 주문이 두 고객으로 나뉘어 구매 횟수와 세그먼트도 틀어졌다. 한 번에 해결되는 방법은 없어서, 단계를 나눠 하나씩 줄여 나갔다. 수취인 정보 우선 사용 : 주문자 정보가 마스킹되어 있어도 배송지( shippingAddress )의 이름·번호는 남아 있는 경우가 있었다. 주문자 값에 * 가 있으면 수취인 값으로 대체했다. 선물 주문이면 다른 사람이 될 수 있지만, 마스킹된 값보다는 연락 가능한 번호가 더 쓸모 있다고 판단했다. 카페24 고객과 패턴 매칭 : 자사몰 고객 중에는 스마트스토어에서도 산 사람이 많다. 마스킹된 값의 * 를 정규식의 . 으로 바꿔 카페24 고객의 전화번호·이름과 맞춰 보고, 정확히 한 명만 맞을 때만 복원했다. 두 명 이상이 맞으면 건드리지 않았다. 이 단계에서 904명을 복원했다. 재동기화 때 자동 복원 : 이미 저장된 주문이 다시 들어왔을 때 API가 실제 정보를 돌려주면, 연결된 마스킹 고객을 실제 값으로 업데이트한다. 네이버 실제 고객과 병합 : 마스킹 고객과 실제 정보 고객이 둘 다 등록된 경우, 같은 길이에 별표 자리를 빼고 모든 글자가 일치하면 같은 사람으로 보고 합쳤다. 2,900명이 병합됐다. 전화번호 중복 병합 : 마지막으로 같은 전화번호로 따로 등록된 고객 118명을 합쳤다. 2단계의 핵심 코드는 다음과 같다. 매칭 결과가 정확히 하나일 때만 반영하는 조건이 가장 중요했다. 마스킹된 번호는 가려진 자리가 많아서 후보가 여러 명 나올 수 있고, 잘못 복원하면 다른 사람에게 문자가 간다. for nc in masked: phone_pat = re.escape(nc.phone).replace("\\*", ".") name_pat = re.escape(nc.name).replace("\\*", ".") try: phone_re = re.compile("^" + phone_pat + "$") name_re = re.compile("^" + name_pat + "$") except re.error: continue matches = [ (name, phone) for phone, name in phone_to_name.items() if phone_re.match(phone) and name_re.match(name) ] if len(matches) == 1: # 후보가 하나일 때만 복원 nc.name, nc.phone = matches[0] updated += 1 새 주문을 저장할 때 쓰는 4단계 매칭은 정규식 대신 글자 단위 비교로 짰다. 동기화마다 마스킹 고객 목록을 한 번만 메모리에 올려 두고, 실제 번호가 들어오면 길이가 같은 후보와 한 글자씩 비교해 * 자리는 통과시킨다. 매칭된 고객은 캐시에서 빼서 같은 마스킹 고객이 두 번 쓰이지 않게 했다. 2. 주문일이 전부 "오늘"로 찍힌 234건 주문 데이터를 점검하다가 주문일이 동기화를 실행한 시각으로 찍힌 주문들을 발견했다. 이런 주문이 있으면 오래전에 한 번 산 고객도 "최근 30일 내 활성 고객"으로 분류된다. 원인은 날짜 파싱의 fallback이었다. 주문 상세 응답에서 결제일·주문일 필드를 순서대로 찾다가 하나도 파싱하지 못하면 datetime.now() 를 넣고 있었다. 일단 저장은 되게 하려던 안전장치가 오히려 데이터를 오염시킨 것이다. 주문일이 틀리면 최종 주문일, 활동 상태, "어제 주문한 고객" 같은 오디언스가 모두 틀어진다. 날짜 후보를 paymentDate , placeOrderDate , 상품주문의 paymentDate , orderDate 순으로 넓혔고, 이미 잘못 저장된 234건은 주문번호( order_id )에 들어 있는 날짜를 추출해 고쳤다. 이 일 이후로 "값이 없으면 현재 시각"을 기본값으로 넣는 코드는 다시 보게 됐다. 날짜가 없으면 차라리 비워 두고 표시하는 편이 나중에 찾기 쉽다. 3. 이미 있는 주문을 어떻게 다룰 것인가 매일 최근 3일을 다시 받기 때문에, 같은 주문이 여러 번 들어온다. 중복 저장만 막으려면 이미 있는 주문은 건너뛰면 된다. 그런데 마스킹 문제를 다루면서 다시 들어온 주문이 오히려 쓸모 있다는 걸 알았다. 이전에는 마스킹돼 있던 주문이 다시 조회될 때 실제 정보로 오는 경우가 있었기 때문이다. 그래서 기존 주문은 주문 데이터를 다시 쓰지는 않되, 연결된 고객이 마스킹 상태이고 이번 응답에 실제 이름·번호가 있으면 고객 정보만 업데이트하고 넘어가도록 바꿨다. 중복 저장은 막으면서 복원 기회는 놓치지 않는 방식이다. 결과 90일 범위를 처음 동기화했을 때 주문 27,300건, 고객 15,900명이 들어왔다. 마스킹 정리 단계에서 카페24 매칭으로 904명을 복원했고, 중복으로 등록된 2,900명과 전화번호가 겹친 118명을 병합했다. 잘못된 주문일 234건도 바로잡았다. 이후 네이버 스마트스토어 화면에서 세그먼트·활동 상태별 고객과 주문 내역을 볼 수 있게 됐고, 오디언스에서는 "전화번호 있음" 조건이 마스킹 번호를 자동으로 빼고 계산한다. 회고 고객 키를 "이름_전화번호"로 정한 것 이 문제를 키웠다. 다시 한다면 처음부터 원본 값과 정규화된 값을 분리해 저장하고, 마스킹 여부를 별도 컬럼으로 두겠다. 지금은 phone LIKE '%*%' 로 마스킹 여부를 판단하는데, 쿼리마다 같은 조건이 반복된다. 병합은 되돌리기 어렵다. 2,900명 규모의 병합은 잘못되면 원상복구가 힘들다. 다음에는 누구를 누구에게 합쳤는지 기록하는 병합 이력 테이블을 먼저 만들고 작업하겠다. "정확히 하나일 때만" 이라는 보수적인 기준은 잘 맞았다. 복원 숫자는 줄었지만, 엉뚱한 사람에게 메시지가 가는 사고는 피할 수 있었다. 기간: 2026.04 ~ 2026.09 기술: Python, Flask, SQLAlchemy, 네이버 커머스 API(OAuth2, bcrypt 서명), APScheduler, PostgreSQL 관련 프로젝트: AI CRM 툴
프로젝트 목록
공개된 기록과 소식
프로필과 소개