원하는 상품이 어디 있는지 몰라서 매장을 헤매야 한다면?
결제를 위해 긴 줄을 서서 기다려야 한다면?
쑈삥끼는 마트에서 고객을 따라다니고, 상품 위치를 안내하며, 결제까지 자동 처리하는 스마트 쇼핑카트 로봇입니다.
QR 스캔으로 고객을 인식하고, LLM + Nav2로 상품 위치를 안내하며, 결제 구역 진입 시 자동 결제가 처리됩니다. 2대가 동시에 각각의 고객을 서빙하며, 경로 충돌은 자체 구현한 Fleet Router로 개발했습니다.
- User / System Requirements
- 맵 디자인 · 시뮬레이션 맵
- 주제·기술 분담 조사
- System Architecture · ERD
- State Diagram · 시나리오
- SLAM 맵 · 실물 맵 완성
- customer_ui · admin_ui
- LLM 가이드 · 인형 추종
- 장바구니 · admin 이동 명령
- Open-RMF → Fleet Router
- 결제 · LED · 음성 시도
- 시나리오 통합 · 데모
📋 Jira Timeline 보기 — 4 Sprints · 67 Issues 클릭하여 펼치기 ▾
- 플랫폼: Pinky Pro (Pinklab) × 2대
- 컴퓨팅: Raspberry Pi 5 (8GB)
- 센서: RPLiDAR + 카메라 + IMU
- 구동: 2륜 구동 · 다이나믹셀
- 출력장치: LCD + LED + 부저
미니어처 마트(188 × 141 cm)에서 로봇 2대를 동시에 운용했습니다. 로봇이 작아서 실제 사람을 인식하기 어렵기 때문에, 커스텀 학습된 YOLOv8 모델로 인형을 추종 대상으로 사용했습니다.
로봇이 IDLE 상태일 때 LCD에 QR 코드가 표시됩니다. 고객이 스마트폰으로 QR을 스캔하면 웹앱에 접속할 수 있고, 카메라 앞에 인형을 보여주면 ReID 특징 벡터와 HSV 색상 히스토그램이 등록되어 추종이 시작됩니다.
로봇이 IDLE 상태일 때 LCD에 QR 코드를 표시합니다. 고객이 스마트폰으로 스캔하면 고객 웹앱에 접속할 수 있습니다.
카메라 앞에 인형을 보여주면 ReID 특징 벡터와 HSV 색상 히스토그램이 등록됩니다. 등록이 완료되면 로봇이 해당 인형을 주인으로 인식하고 추종을 시작합니다.
4단계 파이프라인으로 주인 인형을 실시간 추종합니다.
커스텀 YOLOv8 — SAM3(Segment Anything Model)로 라벨링한 300장의 데이터로 인형 전용 모델을 훈련시켰습니다.
ByteTracker — 프레임 간 동일 객체를 이어붙이는 다중 객체 추적 알고리즘입니다. model.track(img, persist=True)로 적용하며, 각 감지된 인형에 고유 track_id를 부여합니다. 화면에서 나갔다가 다시 들어오면 새로운 ID를 받기 때문에, 아래의 ReID로 주인을 식별해야 합니다.
ReID + HSV — ReID는 이미지를 512차원 특징 벡터로 변환해서 코사인 유사도로 비교합니다. HSV 히스토그램 상관계수(≥0.25)를 보조로 사용하여, 비슷하게 생긴 다른 인형에 대한 오인식을 줄였습니다. 조명 변화에도 강합니다.
PI/P 제어 — bbox 크기로 선속도를 PI 제어합니다. P만 사용하면 TARGET에 닿기 직전 마찰 때문에 멈춰버리는 정상상태 오차가 발생해서 I를 추가했습니다. 각속도는 bbox 중심 X좌표로 P 제어하며, deadzone ±45px을 두어 미세한 떨림을 방지했습니다.
고객이 웹앱에서 "음료수 어디 있어?"라고 텍스트를 입력하면 다음 파이프라인을 거칩니다:
- 입력 방어 — 매장과 무관한 질문 필터링
- 키워드 매칭 — DB 상품명과 직접 매칭 시도
- 시맨틱 검색 — 임베딩 벡터 코사인 유사도로 유사 상품 검색
- LLM 폴백 — Qwen 2.5가 자연어를 분석해 zone_id 반환
- Nav2 자율주행 — DB에서 좌표 조회 후 해당 구역까지 자율 이동, 도착 시 고객에게 알림
스마트폰 카메라로 상품의 QR 코드를 스캔하면 장바구니에 자동으로 추가됩니다. 상품명과 가격이 실시간으로 웹앱에 반영되며, 항목별 삭제도 가능합니다. 결제 시에는 is_paid=0인 항목만 결제 대상이 됩니다.
사용자가 자리를 비울 때 앱에서 [대기하기]를 누르면 로봇이 정지합니다. 대기 중에 RPLiDAR 전방 반원 내에서 근접 통행자를 감지하면 좌우 중 여유 공간이 더 넓은 쪽으로 Nav2 측방 이동(~0.3m)하여 통행로를 확보합니다. 일정 시간 내에 복귀하지 않으면 장바구니 상태에 따라 LOCKED(미결제 물건 있음) 또는 RETURNING(빈 카트)으로 자동 전환됩니다.
BoundaryMonitor가 AMCL 위치 기반으로 체크아웃존 진입을 감지하면 앱에 결제 팝업이 자동으로 표시됩니다. 미결제 상태(TRACKING)에서는 출구 방향 이동이 차단되며, 결제 완료 후 TRACKING_CHECKOUT 상태로 전환되어 출구 통과가 허용됩니다. 결제 구역 안쪽으로 재진입하면 다시 TRACKING으로 전환되어 추가 쇼핑이 가능합니다.
쇼핑이 종료되면 로봇이 Nav2를 통해 충전소로 자율 복귀합니다. 로봇의 현재 위치에 따라 경로가 분기됩니다. 결제구역이나 출구 근처에서는 좁은 통로를 경유(inflation OFF)한 뒤 fleet 그래프 경로로 합류하고, 일반 매장 내부에서는 fleet 그래프 경로로 직행합니다. 미결제 물건이 있을 경우에는 LOCKED 상태로 LED 잠금 신호를 유지한 채 귀환합니다.
PyQt6 기반 관제 앱으로 로봇 2대의 실시간 위치, 상태, 배터리를 모니터링할 수 있습니다. 강제 귀환, 위치 조정, 세션 종료 등의 원격 명령이 가능합니다.
고객 등록 → 추종 → 상품 검색 → 장바구니 → 결제 → 복귀까지의 전체 흐름입니다.
| 분류 | 기술 |
|---|---|
| 로봇 | ROS 2 Jazzy, Nav2, SLAM, Open-RMF |
| 언어 | Python, C++ |
| AI / 인식 | YOLOv8, ByteTrack, ReID, LLM (Qwen 2.5) |
| 시뮬레이션 | Gazebo, RViz |
| 서버 / DB | Flask, SocketIO, PostgreSQL, Docker |
| UI | PyQt6 (Admin), HTML/JS (Customer Web) |
| 하드웨어 | Raspberry Pi 5, RPLiDAR, Dynamixel |
가장 고생했던 부분이 Nav2 파라미터 튜닝이었습니다. 1.4 × 1.8m 미니어처 마트에서 로봇이 선반에 부딪히지 않으면서 좁은 통로를 빠져나가야 하는데, 처음에 inflation_radius를 0.05로 설정했더니 통로 자체를 장애물로 인식해서 경로를 아예 못 찾았습니다. 0.025로 줄여봤더니 이번엔 경로는 찾는데 선반 모서리를 스치면서 지나가서 위험했고요. 결국 0.01까지 내렸더니 겨우 통과가 됐습니다.
그런데 또 다른 문제가 생겼습니다. Gazebo 시뮬레이션에서 완벽하게 돌아가던 값이 실제 Pi 5에 올리니까 planner가 너무 느려서 로봇이 멈칫멈칫 거리는 겁니다. CPU가 버거워하는 거였죠. planner 주기를 10Hz에서 5Hz로 낮추고, BT server timeout도 500ms까지 늘려서야 안정적으로 동작했습니다.
거기에 실제 Map과 Gazebo World 간의 미세한 오차 때문에 시뮬레이션에서는 문제없던 경로가 실제 맵에서는 벽에 걸리는 경우도 있었습니다. 결국 다시 한번 더 실측하여 World를 실제 맵과 거의 동일하게 재구축했습니다.
이런 식으로 커밋 하나에 파라미터 한 줄 바꾸고, 테스트하고, 또 바꾸고를 수십 번 반복했습니다.
| 파라미터 | 시뮬레이션 | 실제 로봇 (Pi 5) | 이유 |
|---|---|---|---|
inflation_radius | 0.05 m | 0.01 m | 좁은 통로 통과를 위해 최소화 |
cost_scaling_factor | 150 | 5.0 | 벽 근처 경로 허용 범위 확대 |
robot_radius | 0.075 m | 0.075 m | 실제 로봇 크기 유지 |
planner_frequency | 10 Hz | 5 Hz | Pi 5 CPU 부하 감소 |
bt_loop_duration | 10 ms | 500 ms | BT 서버 타임아웃 여유 확보 |
bond_timeout | 4 s | 30 s | lifecycle 노드 연결 안정화 |
ROS 2 Jazzy 호환성 문제도 꽤 골치 아팠습니다. collision_monitor의 파라미터 형식이 이전 버전과 달라져서, 처음에는 왜 노드가 안 뜨는지 한참을 헤맸습니다. 알고 보니 polygons와 observation_sources를 empty string_array로 명시해야 했고, docking_server config도 Jazzy에서는 필수로 요구되더군요. 이것만 해결하는 데 7~8번의 커밋이 필요했습니다.
수업 시간에는 로봇 한 대만 자율주행을 해봤기 때문에, 2대를 동시에 돌리는 건 처음이었습니다. 처음에 두 대를 동시에 켰더니 두 로봇이 같은 /cmd_vel 토픽에 명령을 쏘면서 서로의 모터를 제어하는 상황이 벌어졌습니다. 한 대가 전진하라고 하면 다른 한 대도 같이 전진하는 거죠.
예상은 했지만 이걸 해결하기 위해 namespace 격리를 적용했습니다. 각 로봇에 robot_54, robot_18이라는 namespace를 부여해서 토픽을 완전히 분리했습니다.
# 로봇별 독립 실행
ros2 launch shoppinkki_nav nav2_bringup.launch.py \
namespace:=robot_54 \
use_namespace:=true \
robot_id:=54
ros2 launch shoppinkki_nav nav2_bringup.launch.py \
namespace:=robot_18 \
use_namespace:=true \
robot_id:=18그런데 namespace만 분리하면 끝날 줄 알았는데, 하나 해결하면 또 다른 문제가 튀어나왔습니다.
| 문제 | 원인 | 해결 |
|---|---|---|
| 토픽 충돌 | 두 로봇이 같은 /cmd_vel 발행 |
namespace로 /robot_54/cmd_vel, /robot_18/cmd_vel 분리 |
| FastDDS 충돌 | 동일 노드 이름 → publisher 매칭 오류 | 로봇별 고유 노드 이름 부여 |
| AMCL 초기 위치 | Gazebo 좌표 ≠ map frame 좌표 | map frame 기준으로 좌표 재계산 |
| TF 프레임 충돌 | base_link 이름 중복 |
namespace prefix 없이 TF 발행 (bringup이 자동 처리) |
FastDDS에서는 노드 이름이 같으면 publisher가 꼬이는 문제가 있어서 고유 이름을 부여해야 했고, AMCL 초기 위치는 Gazebo world 좌표와 map frame 좌표가 달라서 실제 로봇이 엉뚱한 곳에 잡히는 문제도 있었습니다. 하나하나 해결해나가는 과정이었습니다.
복귀(RETURNING) 경로를 구현할 때 고민이 많았습니다. 단순히 "충전소 좌표로 가라 !"라고 하면 될 것 같지만, 실제로는 그렇게 간단하지 않았습니다. 결제를 마치고 출구 쪽에 있는 로봇이 다시 매장 안쪽을 가로질러서 충전소로 가면 동선이 엉망이 되거든요. 결제구역이나 출구로 나간 로봇은 다시 결제구역으로 돌아갈 수 없게 해야 했습니다.
그래서 로봇의 현재 위치에 따라 경로를 분기하는 방식을 택했습니다.
# 결제구역/출구 근처 판별 (좁은 세로 통로)
LOWER_AREA_THRESHOLD_Y = -1.2
LOWER_AREA_THRESHOLD_X = 0.3
in_lower_area = (cy < LOWER_AREA_THRESHOLD_Y
and cx < LOWER_AREA_THRESHOLD_X)| 현재 위치 | 조건 | 경로 |
|---|---|---|
| 결제구역/출구 근처 | y < -1.2 且 x < 0.3 | 현재 위치 → exit2 → 하단복도 → fleet 그래프 → 충전소 |
| 일반 매장 내부 | 그 외 | 현재 위치 → fleet 그래프 경로 → 충전소 |
결제구역이나 출구 근처는 Nav Graph 노드가 없는 좁은 세로 통로여서, 그래프 경로로는 갈 수가 없었습니다. 그래서 이 구간만 고정 경유점(exit2 → 하단복도)을 inflation OFF로 먼저 통과시키고, 하단복도부터 그래프 경로로 합류하는 방식으로 해결했습니다.
fleet 그래프 경로는 REST API로 control_service에 질의해서 받아옵니다. 중간 경유점의 theta는 다음 노드 방향으로 계산하고, 마지막 충전소 도착 시에는 저장된 주차 방향을 사용합니다.
# fleet 경로 질의
url = f"http://{host}:{port}/fleet/route?from_x={cx}&from_y={cy}&dest={charger_name}&robot_id={robot_id}"
route = requests.get(url).json()["route"] # [{x, y}, ...]
# 중간점 theta = 다음 노드 방향, 마지막 theta = 충전소 저장 orientation
for i, pt in enumerate(route):
if i == len(route) - 1:
theta = slot["waypoint_theta"] # 충전소 주차 방향
else:
theta = atan2(route[i+1].y - pt.y, route[i+1].x - pt.x)충전소 슬롯은 2개(P1=140, P2=141)를 두어 빈 슬롯을 조회한 뒤 이동합니다.
2대의 로봇이 동시에 같은 코너로 이동하면 어떻게 될까? 이 문제를 해결하기 위해 처음에는 오픈소스 미들웨어인 Open-RMF를 도입했습니다. 대규모 로봇 Fleet을 관리하는 플랫폼이라 "이거면 해결되겠다" 싶었거든요.
적용엔 성공을 했습니다. 다만 상황이 달랐습니다. Open-RMF는 수십 대의 이기종 로봇을 전제로 만들어진 플랫폼이라, 고작 2대를 제어하는 데 노드 기동만 8초 이상 걸리고, Raspbery Pi 5에서만 돌아가는 우리 환경에는 조금 무거운 플랫폼이었습니다.
결국 Open-RMF에서 배운 Nav Graph 설정 방식은 그대로 가져가되, 런타임 의존성을 걷어내고 자체 Fleet Router를 직접 구현하기로 했습니다.
| Open-RMF | 자체 Fleet Router | |
|---|---|---|
| 예약 차원 | 시공간 — 궤적 + 시간축 (4D) | 공간 — 그래프 lane 선점 |
| 충돌 해결 | 로봇 간 협상 프로토콜로 궤적 수정 | Dijkstra 페널티 가중치로 자동 우회 |
| 아키텍처 | 분산 — Fleet Adapter + Schedule 노드 | 중앙 집중 — FleetRouter 단일 노드 |
| 적합 규모 | 대규모·이기종 Fleet | 2대 규모에 단순·충분 |
자체 Fleet Router에서는 3가지 유형의 충돌을 감지하고 처리합니다.
| 충돌 유형 | 설명 | 해결 방식 |
|---|---|---|
E_SHARE | 두 로봇이 같은 lane을 동시에 예약 | 예약된 lane에 페널티 → Dijkstra 우회 |
E_OPPOSE | 두 로봇이 같은 lane을 반대 방향으로 진입 | 우선순위 낮은 로봇이 holding point에서 대기 |
V_CONVERGE | 두 로봇이 같은 꼭짓점으로 수렴 | 남은 웨이포인트 수로 양보 우선순위 결정 |
돌이켜보면, Open-RMF를 먼저 경험한 덕분에 Nav Graph 기반 경로 조율이 어떻게 동작하는지 이해할 수 있었고, 그 위에서 우리 규모에 맞는 단순한 솔루션을 설계할 수 있었습니다.
- 협업이 잘 되었다 — 역할 분담이 명확했고, 덕분에 시스템 통합이 원활하게 진행되었습니다
- 커스텀 YOLO + ReID + HSV 파이프라인이 예상보다 안정적으로 동작했습니다
- ROS2 상태 머신 기반 아키텍처 덕분에 기능 간 충돌 없이 안정적인 전환이 가능했습니다
- 멀티로봇 경로 조율 — 자체 Fleet Router로 2대의 로봇 간 경로 충돌을 해결하고, 순차 웨이포인트 내비게이션으로 안정적인 복귀 경로를 구현할 수 있었습니다
- 음성 인식 미구현 — 텍스트 입력 대신 실제 음성으로 가이드를 요청하고 싶었습니다
- 하드웨어 제약으로 실제 사람을 추종하는 것을 해보지 못했습니다
- 구현하지 못한 시나리오들 — LOST 처리, LCD 표시, LED·부저 피드백, 실제 자동결제 구현 등
- Open-RMF → 자체 구현 — 오픈소스를 적용해보고, 프로젝트 규모에 맞지 않는 부분을 판단하여 우리 프로젝트에 맞춰 직접 구현하는 경험을 했습니다. 의존성을 걷어내고 Fleet Router를 자체 구현하며 경로 조율의 복잡성을 체감했습니다
- 하드웨어 제약 안에서의 설계 — 카메라와 연산의 한계를 소프트웨어로 극복하는 경험을 했습니다
- 설계의 중요성 — 초반에 인터페이스와 아키텍처 설계에 투자한 시간이 후반 통합을 빠르게 만들어주었습니다
더 자세한 후기는 프로젝트 후기 블로그 글에서 확인할 수 있습니다.
🏗️ 시스템 아키텍처 클릭하여 펼치기 ▾

시스템은 UI / SERVER / EQUIP 세 개의 레이어로 구성됩니다.
| 레이어 | 컴포넌트 | 역할 |
|---|---|---|
| UI | Customer UI (스마트폰 브라우저) | 고객용 웹앱 — 상품 검색, 장바구니, 결제, 로봇 상태 확인 |
| UI | Admin UI (관제 PC) | PyQt 관리자 관제앱 — 로봇 모니터링, 알람, 강제 명령 |
| SERVER | Customer Web (Flask + SocketIO) | 고객 UI 서빙 및 Control Service 중계 |
| SERVER | AI Server (Docker) | YOLOv8 추론 서버 (TCP) + LLM 검색 서버 (REST) |
| SERVER | Control Service (ROS2 + REST) | 로봇 명령·상태 관리, PostgreSQL DB 전담 |
| EQUIP | shoppinkki_core (Pi 5) | 상태머신 + 행동트리 통합 노드, HW 제어 |
| EQUIP | shoppinkki_nav (Pi 5) | Nav2 BT (Guiding / Returning / Waiting 회피) |
| EQUIP | shoppinkki_perception (Pi 5) | YOLO 인형 감지 / QR 스캔 |
🔄 상태 머신 / FSM 클릭하여 펼치기 ▾

로봇은 10개 상태를 가지며, transitions 라이브러리 기반 FSM으로 전환을 관리합니다.
| 상태 | 설명 |
|---|---|
CHARGING | 초기 상태, 충전 대기 |
IDLE | 사용자 등록 대기 (LCD에 QR 표시) |
TRACKING | 주인 인형 추종 중 |
TRACKING_CHECKOUT | 결제 완료 후 추종 (출구 통과 허용) |
GUIDING | Nav2로 상품 위치 안내 이동 |
SEARCHING | 주인 놓침 → 제자리 회전 재탐색 (30초) |
WAITING | 정지 대기, 통행자 감지 시 회피 |
LOCKED | 미결제 물건 있는 상태로 귀환 |
RETURNING | 충전소로 자율 복귀 |
HALTED | 배터리 부족 → 즉시 정지 |
🗺️ Nav Graph 클릭하여 펼치기 ▾

매장 내 로봇 이동 경로를 정의하는 28개 꼭짓점의 Nav Graph입니다. 충전소, 결제구역, 픽업존, 선반 우회점 등으로 구성되며 실좌표 기반으로 그리드 정렬되어 있습니다.
상세 문서: 매장 지도 설계
🗄️ ERD 클릭하여 펼치기 ▾

PostgreSQL 기반 중앙 DB로, USER / SESSION / CART / ROBOT / ZONE 등 11개 테이블로 구성됩니다.
상세 문서: ERD 설계
| 이름 | 역할 |
|---|---|
| 박우림 | 프로젝트 리딩 & 일정 관리, 전체 시스템 아키텍처 설계, 서버·로봇 통신 프레임워크, 상태 머신 & 행동 트리 설계, Gazebo 시뮬레이션 맵 제작 |
| 최민성 | 매장 공간 설계 (CAD), 로봇 자율주행 기반 구축 (SLAM, Nav2), Open-RMF로 다중 로봇 경로 조율, 가이드 / 복귀 / 충전 자율주행 |
| 이정우 | 커스텀 YOLOv8 학습, SAM3 마스크 생성, IoU 트래커 구현, ReID + HSV 추종 파이프라인, 장바구니 3D 프린팅 |
| 이강택 | 실물 미니어처 매장 제작, 로그인·장바구니·대기 기능, QR 스캔 장바구니 관리, 결제 구역 감지 & 자동 결제 처리, Customer Web 맵 UI |
| 이지수 | 실물 미니어처 매장 제작, LLM 서버 구축 (Qwen 2.5 3B / Ollama), pgvector 자연어 구역 벡터 검색, 가이드 UI, LLM 답변 정확도 개선 |
프로젝트 발표(04.22) 이후에도 회고에서 아쉬웠던 부분들과 실제 운용 중 발견된 문제들을 지속적으로 개선했습니다.
회원가입 & 카드 관리 시스템 — 프로젝트 기간에는 사전 등록된 유저만 사용할 수 있었습니다. 회원가입 페이지와 API를 새로 구현하고, 결제 전 카드 미등록 시 등록 모달을 띄우도록 개선했습니다. 비밀번호는 bcrypt로 해싱하여 저장합니다.
관리자 유저/세션 관리 패널 — 기존 Admin UI에는 로봇 모니터링만 있었는데, 유저 목록 조회·삭제와 활성 세션 관리 기능을 추가했습니다. FK 제약 조건을 고려하여 유저 삭제 시 세션을 먼저 정리하는 로직도 함께 구현했습니다.
LED 상태 피드백 — 로봇의 상태머신 상태에 따라 WS2812B LED 스트립의 색상이 자동으로 변경되도록 구현했습니다. 충전(빨강), 대기(흰색), 추종(초록), 안내(노랑), 검색/대기(파랑), 복귀(보라), 미결제 잠금(빨강 깜빡임) 등 상태별 색상을 매핑하여 고객과 관리자가 로봇의 현재 상태를 직관적으로 파악할 수 있도록 했습니다.
| 영역 | 문제 | 수정 내용 |
|---|---|---|
| Nav2 | bt_navigator의 frame이 불일치하여 간헐적 TF 오류 발생 | base_footprint으로 통일 |
| Nav2 | collision_monitor 노드 누락으로 다중 로봇 충돌 감지 불가 | 실제 로봇 launch에 노드 추가 |
| 상태 머신 | GUIDING 중 Nav 실패 시 WAITING으로 빠져서 복구 불가 | resume_tracking으로 추종 복귀 |
| 상태 머신 | TRACKING/TRACKING_CHECKOUT에서 LOCKED 전환 불가 | 누락된 상태 전환 경로 추가 |
| 결제 | checkout zone 경계가 웨이포인트(0.186)를 포함하지 않아 결제 미감지 | x_max 범위 확장 |
| HW 제어 | LED·감정 표시 서비스 타입 오류로 로봇 피드백 미작동 | 올바른 서비스 타입으로 수정 |
| 배터리 | 저전압 감지 미구독으로 HALTED 전환 불가 | battery/percent 토픽 구독 추가 |
- XSS 방지 — Customer Web의 동적 값 innerHTML 삽입 시 이스케이프 처리 추가
- SECRET_KEY 안전 생성 — 환경변수 미설정 시 랜덤 키 자동 생성
- 로깅 표준화 —
print()→logger.info()로 전환하여 운영 로그 추적 가능 - 설정 중앙화 —
RETURN_RELAY_MODES,CHARGER_WAYPOINT_NAMES등 하드코딩된 값을 config로 분리 - 임시 파일 정리 — Nav2 파라미터 임시 파일이 프로세스 종료 시 자동 삭제되도록 개선