포트폴리오
프로젝트 목록
송장 엑셀을 들고 한 줄씩 대조하던 CS 마더케이의 CS 업무는 생각보다 갈래가 많다. 챗봇, 게시판, 전화 인입(IN)과 발신(OUT), 메일, 다른 부서에서 넘어오는 연계 건이 하나의 CS 시스템에 쌓이고, 이 가운데 일부는 재출고, 교환반품, 현금환불로 이어진다. 기록 자체는 사내 웹 시스템에 남고 있었다. 문제는 기록 다음 단계였다. AS가 대표적이었다. 고객이 맡긴 제품을 점검하고 다시 보내 주면 택배사 송장이 엑셀로 나온다. 담당자는 그 엑셀을 열어 놓고 접수 목록에서 고객을 한 명씩 찾아 송장번호를 옮겨 적고, 상태를 바꾸고, 고객에게 출고 안내를 따로 보냈다. 담당자 목록도 코드 안에 이름 배열로 박혀 있어서 사람이 바뀌면 개발자가 파일을 고쳐야 했다. 퇴사자는 목록에 남고, 새로 온 사람은 목록에 없었다. 이 글은 그 흐름을 하나로 묶은 과정의 첫 번째 기록이다. CS 인입 기록에서 시작해 AS 송장 처리와 알림톡, 담당자 연동, 낡은 화면 정리까지를 다룬다. 스마트스토어 리뷰 답변 봇은 이야기가 길어 2편으로 따로 뺐다. 해야 했던 일과 걸림돌 AS 출고 처리 자동화 : 택배사 송장 엑셀을 올리면 해당 접수 건을 찾아 송장을 넣고 상태를 바꾸고, 고객에게 카카오 알림톡까지 보내야 했다. 담당자 관리 : CS 담당자는 인사정보 기준으로 자동으로 바뀌어야 하지만, 이미 쌓인 상담 기록의 담당자 이름은 그대로 남아야 했다. 기존 화면 유지 : cs/ 아래 PHP 파일이 수십 개였고, 백업본( main copy.php , main_bak.php 등)까지 섞여 있었다. 현업이 매일 쓰는 화면이라 한 번에 갈아엎을 수 없었다. 제약도 분명했다. 고객 이름과 전화번호 말고는 송장 엑셀과 AS 접수 건을 이어 줄 공통 키가 없었다. 한 사람이 여러 번 AS를 맡기는 경우도 있다. CS 데이터는 cs.datas 한 테이블에 등록·재출고·반품·환불이 모두 같은 행으로 누적되는 구조였고, 상품명 컬럼이 product_name 부터 product_name10 까지 펼쳐져 있었다. 이 구조를 바꾸는 대신 그 위에서 일하는 쪽을 택했다. 구조와 기술 선택 전체 시스템은 사내 통합 관리 도구(mkware) 안에 들어 있다. PHP 8과 MariaDB 위에서 돌고, CS 관련 화면은 크게 세 덩어리다. 영역 역할 CS ( cs/ , pages/cs ) 인입 등록, 재출고·교환반품·현금환불, 불량 유형 집계, 주간 보고서 AS ( pages/as ) AS 접수, 회수 송장, 발송 송장 업로드, AS 리포트 리뷰 ( cs_review_02.php , 리뷰 봇) 스마트스토어 리뷰 답글 초안·게시 상태와 AI 호출 비용 조회 새 프레임워크를 들이지 않고 기존 PHP 구조를 유지한 이유는 단순하다. 로그인과 세션, 인사정보( mkmall.member )가 이미 이 시스템을 기준으로 돌고 있었고, 현업 화면이 매일 쓰이고 있었다. 대신 쿼리는 PDO/mysqli prepared statement로 바꾸고, 공통 판단 로직은 작은 함수 파일로 빼서 테스트할 수 있게 했다. 엑셀 읽기는 PhpSpreadsheet, 알림톡은 네이버 클라우드 SENS의 알림톡 API를 서명 헤더와 함께 호출했다. 구현 과정과 시행착오 1. AS 송장 업로드: "아무 접수 건"에 송장이 붙었다 처음 만든 송장 업로드는 단순했다. 엑셀 2행부터 읽어 J열 이름, M열 전화번호, U열 송장번호를 꺼내고, 이름과 전화번호(하이픈·공백 제거)가 맞는 가장 최근 접수 건에 송장을 넣은 뒤 상태를 배송중 으로 바꿨다. 실제 택배사 양식으로 돌려 보니 두 군데가 어긋났다. 양식의 헤더가 두 줄이라 2행을 데이터로 읽고 있었다. 더 큰 문제는 상태 조건이 없다는 점이었다. 같은 고객이 예전에 맡긴 AS가 있으면, 아직 점검도 끝나지 않은 접수 건이나 이미 처리가 끝난 접수 건에도 송장이 붙을 수 있었다. 상태 이름도 AS 업무에서 실제로 쓰는 단계와 맞지 않았다. 그래서 읽기 시작 행을 3행으로 옮기고, 매칭 조건에 status = '점검완료' 를 넣었다. 점검이 끝나 출고만 남은 건에만 송장이 들어가고, 상태는 출고완료 , 송장은 회수 송장과 구분되는 delivery_invoice 컬럼에 저장한다. 매칭이 안 된 행은 버리지 않고 "일치하는 접수건 없음 (점검완료 상태만 처리)"로 결과 표에 남겨 담당자가 직접 확인하게 했다. SELECT id, pickup_name, pickup_phone, delivery_name, delivery_phone, status, created_at FROM as.as_requests WHERE ((pickup_name = ? AND REPLACE(REPLACE(pickup_phone, '-', ''), ' ', '') = ?) OR (delivery_name = ? AND REPLACE(REPLACE(delivery_phone, '-', ''), ' ', '') = ?)) AND status = '점검완료' ORDER BY created_at DESC LIMIT 1 회수자와 수령자가 다른 경우가 있어 두 쌍을 모두 본다. 바깥 괄호가 없으면 AND status 가 뒤쪽 조건에만 붙기 때문에 괄호를 한 겹 더 씌웠다. 2. 알림톡은 커밋이 끝난 뒤에 송장 처리에 알림톡을 붙일 때 고민한 것은 순서였다. 행마다 DB를 고치면서 바로 알림톡을 보내면, 중간에 예외가 나서 트랜잭션이 롤백될 경우 DB에는 출고 처리가 안 됐는데 고객은 출고 안내를 받는 상황이 생긴다. 그래서 엑셀 전체를 하나의 트랜잭션으로 처리하고, 커밋이 끝난 뒤에 성공한 건만 모아서 알림톡을 보냈다. 알림톡 실패는 출고 처리를 되돌리지 않고 결과 표의 "알림톡" 칸에 실패 사유와 함께 표시한다. $pdo->commit(); // 카카오 알림톡 발송 (커밋 후) $deliveryDate = date('Y-m-d'); foreach ($results['details'] as &$detail) { if ($detail['status'] === 'success' && isset($detail['request_data'])) { $req = $detail['request_data']; $cleanPhone = preg_replace('/[^0-9]/', '', $req['pickup_phone']); $kakao = sendKakaoProductExchange($cleanPhone, $req['pickup_name'], '교환 및 AS 처리 완료', $deliveryDate, $detail['tracking']); $detail['kakao'] = $kakao['success']; if ($kakao['success']) $results['kakao_sent']++; else $detail['kakao_error'] = $kakao['message']; unset($detail['request_data']); } } 업로드가 끝나면 "N건 성공, M건 미매칭, 알림톡 K건 발송"이 한 줄로 뜨고, 아래 표에 행 번호별 결과가 나온다. 담당자가 다시 해야 할 일은 미매칭과 알림톡 실패 줄만 보는 것이다. 3. 담당자 목록: 하드코딩 배열에서 인사정보로 CS 대시보드의 담당자별 차트는 코드 안의 이름 배열 세 개를 돌며 담당자마다 쿼리를 한 번씩 날리고 있었다. 사람이 바뀌면 차트가 틀렸고, 입력 폼의 담당자 칸은 자유 입력이라 오타가 그대로 쌓였다. 처음엔 인사정보에서 CS팀 인원을 불러오면 끝날 줄 알았다. 그런데 그렇게 하면 퇴사자가 담당했던 과거 상담을 열었을 때 담당자 칸이 비거나 다른 사람으로 바뀌어 저장될 수 있었다. 과거 기록은 이름 단위로만 남아 있어서 사번으로 이어 줄 수도 없었다. 그래서 "새로 지정할 수 있는 사람"과 "기록에 남아 있는 사람"을 나눴다. layouts/cs_staff.php 에 인사정보 한 줄을 재직·퇴사·타 부서·입사 예정 상태로 판정하는 함수를 두고, 저장할 때는 아래 규칙만 통과시킨다. function cs_staff_assignment_allowed(string $name, array $directory, ?string $previous = null): bool { // 기존 담당자를 그대로 두는 것은 허용, 새로 지정할 때는 CS팀 재직자만 return ($previous !== null && $name === $previous) || ($directory[trim($name)] ?? '') === 'active'; } 수정 화면은 재직자만 선택지로 보여 주되, 기존 담당자가 목록에 없으면 "· 기존 담당자" 꼬리표를 단 선택지를 하나 더 붙인다. 목록 필터에는 재직자와 과거 기록의 담당자를 합쳐 "재직 / 퇴사 / 과거 담당자" 라벨을 달았다. 대시보드는 담당자마다 쿼리를 날리던 방식을 버리고 GROUP BY representative 한 번으로 바꿨고, "재직 담당자"와 "전체 담당자 이력" 두 범위를 오갈 수 있게 했다. 같은 이름의 계정이 여러 개면 재직 계정을 우선한다. 이 판정 함수들은 tests/cs_staff.php 로 따로 검증했다. 4. 낡은 저장 스크립트 정리 화면을 현대화하면서 저장 스크립트도 들여다봤다. 불량 내역 저장 같은 파일은 real_escape_string 으로 값을 감싼 뒤 문자열로 SQL을 조립하고, 오류가 나면 DB 오류 메시지를 그대로 응답에 실어 보냈다. 맨 위에는 display_errors 가 켜져 있었다. 이런 파일들을 공통 처리 파일( fault_action.php , status_action.php , action_support.php )로 모았다. POST와 로그인 여부를 먼저 확인하고, 문서번호는 정수로 검증하고, 값은 prepared statement로 넘겨 입력한 원문이 그대로 저장되게 했다. 내부 오류는 서버 로그에만 남기고 사용자에게는 "잠시 후 다시 시도해 주세요"만 보여 준다. 사진 첨부도 파일당 10MB, 최대 3장, JPG·PNG·GIF만 받도록 업로드 전에 검사한다. 기존 save_data.php 는 지우지 않고 새 처리 파일을 불러오는 두 줄짜리 파일로 남겨 기존 화면의 호출 경로를 깨지 않았다. 결과 AS 담당자는 택배사 송장 엑셀 하나를 올리면 점검완료 건의 출고 처리와 고객 알림톡까지 끝낸다. 남는 일은 미매칭 줄 확인이다. CS 담당자 목록은 인사정보의 재직자 기준으로 자동 표시되고, 과거 상담 기록의 담당자 이름은 바뀌지 않는다. 퇴사자가 담당했던 기록도 필터로 찾을 수 있다. CS 전체 화면이 같은 스타일과 저장 규칙으로 정리됐고, 오류 메시지가 사용자 화면에 새지 않는다. 이 시스템과 별개로 분리배송 알림톡 발송 프로그램(Python, Tkinter)도 함께 손봤다. DB 직접 접속을 걷어내고 엑셀 입력으로 바꿨고, 택배사 송장번호 접두사가 바뀌어 판별 조건을 고쳤다. PyInstaller로 빌드하면 드래그앤드롭용 네이티브 tkdnd 라이브러리가 번들되지 않아 실행 직후 죽는 문제가 있었는데, 초기화를 try/except로 감싸 일반 Tk 창으로 폴백하게 했다. 회고 AS 송장 매칭에서 배운 건, 이름과 전화번호처럼 약한 키로 매칭할 때는 "어떤 상태의 건이 대상인가"를 먼저 못 박아야 한다는 점이다. 처음 버전은 매칭 자체는 잘 됐지만 대상 범위가 넓어서 틀린 건에 붙을 위험이 있었다. 다시 만든다면 처음부터 업로드 전 미리보기 단계를 두고, 매칭 결과를 확인한 뒤 확정 버튼을 누르게 할 것이다. 지금은 결과를 업로드 후에 보여 준다. 담당자 연동은 기록을 이름으로 저장해 둔 과거 설계의 대가를 치른 셈이다. 지금 구조에서는 동명이인을 완전히 가를 방법이 없다. 새로 쌓이는 기록부터라도 사번 같은 안정된 키를 함께 저장했다면 나중이 훨씬 편했을 것이다. 그리고 수십 개의 PHP 파일을 한 번에 바꾸는 대신, 기존 파일을 얇은 진입점으로 남기고 공통 처리 파일로 모은 방식은 현업 화면을 멈추지 않고 정리하는 데 잘 맞았다. 기간: 2026.02 ~ 2026.09 기술: PHP 8, MariaDB, PDO/mysqli, PhpSpreadsheet, 네이버 클라우드 SENS(카카오 알림톡), Chart.js, Bootstrap, Python(Tkinter, PyInstaller)
프로젝트 목록
공개된 기록과 소식
프로필과 소개