feat(cupang): 센터 중복 등록 알림 + 목록 지역별 그룹화
추가 시 같은 이름이 있으면 조용히 재활성되던 동작(ON CONFLICT DO UPDATE) 때문에 사용자가 중복을 알 수 없었다. 등록 전에 먼저 조회해서 이미 있으면 만들지 않고 알림(활성/비활성 구분)을 띄운다. 목록은 29개가 가로 4열로 흩어져 훑기 어려웠다. 센터명 앞 지역 접두사로 묶고(인천14 → "인천") CSS 멀티컬럼으로 한 열을 위→아래로 읽게 바꿨다. 숫자는 자연 정렬(인천4 < 인천14). 행 액션은 hover/focus 시에만 노출해 이름이 먼저 눈에 들어오게 했다. - 라우터: _center_group / _center_sort_key / _group_centers 추가 - 검색은 그룹 단위로 접히고 결과 0건이면 안내 표시 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,8 +11,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import calendar as _calendar
|
import calendar as _calendar
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
from app.timezone import today_kst
|
from app.timezone import today_kst
|
||||||
|
|
||||||
@@ -408,6 +410,34 @@ async def hard_delete(
|
|||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 입고센터 관리
|
# 입고센터 관리
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
|
_CENTER_PREFIX_RE = re.compile(r"^\D*")
|
||||||
|
|
||||||
|
|
||||||
|
def _center_group(name: str) -> str:
|
||||||
|
"""센터명 앞부분(숫자 전까지)을 지역 그룹 키로 쓴다. 예) 인천14 → 인천."""
|
||||||
|
name = (name or "").strip()
|
||||||
|
prefix = _CENTER_PREFIX_RE.match(name).group(0).strip()
|
||||||
|
return prefix or name or "기타"
|
||||||
|
|
||||||
|
|
||||||
|
def _center_sort_key(name: str) -> list[Any]:
|
||||||
|
"""숫자를 숫자로 비교하는 자연 정렬. 예) 인천4 < 인천14."""
|
||||||
|
parts = re.split(r"(\d+)", (name or "").strip())
|
||||||
|
return [(1, int(p), "") if p.isdigit() else (0, 0, p.lower()) for p in parts]
|
||||||
|
|
||||||
|
|
||||||
|
def _group_centers(centers: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
|
buckets: dict[str, list[dict[str, Any]]] = {}
|
||||||
|
for c in centers:
|
||||||
|
buckets.setdefault(_center_group(c["name"]), []).append(c)
|
||||||
|
groups = []
|
||||||
|
for key, items in buckets.items():
|
||||||
|
items.sort(key=lambda c: _center_sort_key(c["name"]))
|
||||||
|
groups.append({"name": key, "items": items, "count": len(items)})
|
||||||
|
groups.sort(key=lambda g: _center_sort_key(g["name"]))
|
||||||
|
return groups
|
||||||
|
|
||||||
|
|
||||||
@router.get("/centers", response_class=HTMLResponse)
|
@router.get("/centers", response_class=HTMLResponse)
|
||||||
async def centers_page(request: Request) -> HTMLResponse:
|
async def centers_page(request: Request) -> HTMLResponse:
|
||||||
from app.main import build_erp_nav, render_template # noqa: WPS433
|
from app.main import build_erp_nav, render_template # noqa: WPS433
|
||||||
@@ -417,7 +447,10 @@ async def centers_page(request: Request) -> HTMLResponse:
|
|||||||
if not isinstance(guard, tuple):
|
if not isinstance(guard, tuple):
|
||||||
return guard
|
return guard
|
||||||
store, user = guard
|
store, user = guard
|
||||||
centers = sorted(store.list_centers(include_inactive=True), key=lambda c: c["name"])
|
centers = sorted(
|
||||||
|
store.list_centers(include_inactive=True),
|
||||||
|
key=lambda c: _center_sort_key(c["name"]),
|
||||||
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
request,
|
request,
|
||||||
"cupang/centers.html",
|
"cupang/centers.html",
|
||||||
@@ -428,6 +461,9 @@ async def centers_page(request: Request) -> HTMLResponse:
|
|||||||
"page_title": "쿠팡 밀크런 — 입고센터 관리",
|
"page_title": "쿠팡 밀크런 — 입고센터 관리",
|
||||||
"page_subtitle": "센터명 등록 · 수정 · 활성/비활성 · 삭제.",
|
"page_subtitle": "센터명 등록 · 수정 · 활성/비활성 · 삭제.",
|
||||||
"centers": centers,
|
"centers": centers,
|
||||||
|
"center_groups": _group_centers(centers),
|
||||||
|
"flash": request.query_params.get("msg", ""),
|
||||||
|
"flash_name": request.query_params.get("name", ""),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -442,11 +478,32 @@ async def center_create(
|
|||||||
store = _store(request)
|
store = _store(request)
|
||||||
if store is None:
|
if store is None:
|
||||||
raise HTTPException(status_code=503, detail="cupang_db 미설정")
|
raise HTTPException(status_code=503, detail="cupang_db 미설정")
|
||||||
|
name = (name or "").strip()
|
||||||
|
if not name:
|
||||||
|
return RedirectResponse(url="/cupang/centers", status_code=303)
|
||||||
|
|
||||||
|
# 같은 이름이 이미 있으면 새로 만들지 않고 알림만 돌려준다.
|
||||||
|
existing = next(
|
||||||
|
(
|
||||||
|
c
|
||||||
|
for c in store.list_centers(include_inactive=True)
|
||||||
|
if (c.get("name") or "").strip().lower() == name.lower()
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if existing is not None:
|
||||||
|
msg = "dup" if existing.get("active") else "dup_inactive"
|
||||||
|
return RedirectResponse(
|
||||||
|
url=f"/cupang/centers?msg={msg}&name={quote(name)}", status_code=303
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
store.create_center(name=name, sort_order=sort_order)
|
store.create_center(name=name, sort_order=sort_order)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc))
|
raise HTTPException(status_code=400, detail=str(exc))
|
||||||
return RedirectResponse(url="/cupang/centers", status_code=303)
|
return RedirectResponse(
|
||||||
|
url=f"/cupang/centers?msg=added&name={quote(name)}", status_code=303
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/centers/{center_id}/edit")
|
@router.post("/centers/{center_id}/edit")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831b" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831c" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831b" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831c" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831b" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831c" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
@@ -24,47 +24,78 @@
|
|||||||
|
|
||||||
<div class="cpg-card-head cpg-center-head">
|
<div class="cpg-card-head cpg-center-head">
|
||||||
<h2>센터 목록 <span class="erp-muted" id="cpgCenterCount">({{ centers|length }})</span></h2>
|
<h2>센터 목록 <span class="erp-muted" id="cpgCenterCount">({{ centers|length }})</span></h2>
|
||||||
<span class="erp-muted">쓰지 않는 센터는 비활성 또는 삭제. 이름 변경은 지원하지 않음 — 새로 등록.</span>
|
<span class="erp-muted">지역별로 묶어 표시. 이름 변경은 지원하지 않음 — 삭제 후 새로 등록.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 목록: 여러 열로 한 화면에 -->
|
<!-- 목록: 지역 그룹 카드, 위→아래로 읽고 열로 흐름 -->
|
||||||
<div class="cpg-center-grid" id="cpgCenterGrid">
|
<div class="cpg-center-cols" id="cpgCenterGrid">
|
||||||
{% for c in centers %}
|
{% for g in center_groups %}
|
||||||
|
<div class="cpg-center-group" data-group="{{ g.name|lower }}">
|
||||||
|
<div class="cpg-center-gtitle">
|
||||||
|
<span class="cpg-center-gname">{{ g.name }}</span>
|
||||||
|
<span class="cpg-center-gcount">{{ g.count }}</span>
|
||||||
|
</div>
|
||||||
|
{% for c in g["items"] %}
|
||||||
<div class="cpg-center-row {% if not c.active %}is-inactive{% endif %}" data-name="{{ c.name|lower }}">
|
<div class="cpg-center-row {% if not c.active %}is-inactive{% endif %}" data-name="{{ c.name|lower }}">
|
||||||
<span class="cpg-center-dot" title="{% if c.active %}활성{% else %}비활성{% endif %}"></span>
|
<span class="cpg-center-dot" title="{% if c.active %}활성{% else %}비활성{% endif %}"></span>
|
||||||
<span class="cpg-center-name">{{ c.name }}</span>
|
<span class="cpg-center-name">{{ c.name }}</span>
|
||||||
|
<span class="cpg-center-acts">
|
||||||
<form method="post" action="/cupang/centers/{{ c.id }}/edit" class="cpg-inline-form">
|
<form method="post" action="/cupang/centers/{{ c.id }}/edit" class="cpg-inline-form">
|
||||||
<input type="hidden" name="active" value="{% if c.active %}0{% else %}1{% endif %}" />
|
<input type="hidden" name="active" value="{% if c.active %}0{% else %}1{% endif %}" />
|
||||||
<button type="submit" class="erp-btn erp-btn-outline cpg-btn-sm"
|
<button type="submit" class="cpg-icon-btn"
|
||||||
title="{% if c.active %}비활성으로 전환{% else %}활성으로 전환{% endif %}">{% if c.active %}비활성{% else %}활성{% endif %}</button>
|
title="{% if c.active %}비활성으로 전환{% else %}활성으로 전환{% endif %}">{% if c.active %}비활성{% else %}활성{% endif %}</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" action="/cupang/centers/{{ c.id }}/delete" class="cpg-inline-form"
|
<form method="post" action="/cupang/centers/{{ c.id }}/delete" class="cpg-inline-form"
|
||||||
onsubmit="return confirm('{{ c.name }} 센터를 삭제할까요?');">
|
onsubmit="return confirm('{{ c.name }} 센터를 삭제할까요?');">
|
||||||
<button type="submit" class="erp-btn erp-btn-danger cpg-btn-sm">삭제</button>
|
<button type="submit" class="cpg-icon-btn is-danger" title="삭제">삭제</button>
|
||||||
</form>
|
</form>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="cpg-center-empty erp-muted" id="cpgCenterEmpty" hidden>검색 결과 없음</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
// 추가 결과 알림 (중복 등록 방지)
|
||||||
|
var flash = {{ flash|tojson }};
|
||||||
|
var fname = {{ flash_name|tojson }};
|
||||||
|
if (flash === 'dup') {
|
||||||
|
alert('"' + fname + '" 센터는 이미 등록되어 있습니다.');
|
||||||
|
} else if (flash === 'dup_inactive') {
|
||||||
|
alert('"' + fname + '" 센터는 이미 등록되어 있습니다. (현재 비활성 상태 — 목록에서 활성으로 전환하세요.)');
|
||||||
|
}
|
||||||
|
if (flash) {
|
||||||
|
history.replaceState(null, '', '/cupang/centers');
|
||||||
|
}
|
||||||
|
|
||||||
var box = document.getElementById('cpgCenterSearch');
|
var box = document.getElementById('cpgCenterSearch');
|
||||||
var grid = document.getElementById('cpgCenterGrid');
|
var grid = document.getElementById('cpgCenterGrid');
|
||||||
var count = document.getElementById('cpgCenterCount');
|
var count = document.getElementById('cpgCenterCount');
|
||||||
|
var empty = document.getElementById('cpgCenterEmpty');
|
||||||
if (!box || !grid) return;
|
if (!box || !grid) return;
|
||||||
var rows = Array.prototype.slice.call(grid.querySelectorAll('.cpg-center-row'));
|
var groups = Array.prototype.slice.call(grid.querySelectorAll('.cpg-center-group'));
|
||||||
box.addEventListener('input', function () {
|
box.addEventListener('input', function () {
|
||||||
var q = box.value.trim().toLowerCase();
|
var q = box.value.trim().toLowerCase();
|
||||||
var n = 0;
|
var n = 0;
|
||||||
rows.forEach(function (r) {
|
groups.forEach(function (g) {
|
||||||
|
var shown = 0;
|
||||||
|
Array.prototype.forEach.call(g.querySelectorAll('.cpg-center-row'), function (r) {
|
||||||
var hit = !q || (r.dataset.name || '').indexOf(q) !== -1;
|
var hit = !q || (r.dataset.name || '').indexOf(q) !== -1;
|
||||||
r.style.display = hit ? '' : 'none';
|
r.style.display = hit ? '' : 'none';
|
||||||
if (hit) n += 1;
|
if (hit) shown += 1;
|
||||||
|
});
|
||||||
|
g.style.display = shown ? '' : 'none';
|
||||||
|
n += shown;
|
||||||
});
|
});
|
||||||
if (count) count.textContent = '(' + n + ')';
|
if (count) count.textContent = '(' + n + ')';
|
||||||
|
if (empty) empty.hidden = n !== 0;
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831b" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831c" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831b" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831c" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831b" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831c" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831b" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831c" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
+49
-13
@@ -169,7 +169,7 @@
|
|||||||
.cpg-inline-form { display: inline-flex; gap: 6px; align-items: center; margin: 0; }
|
.cpg-inline-form { display: inline-flex; gap: 6px; align-items: center; margin: 0; }
|
||||||
.cpg-row-actions { display: flex; gap: 6px; flex-wrap: wrap; }
|
.cpg-row-actions { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||||
|
|
||||||
/* ── 입고센터 관리 (상단: 추가·검색 / 아래: 목록 그리드) ── */
|
/* ── 입고센터 관리 (상단: 추가·검색 / 아래: 지역 그룹 목록) ── */
|
||||||
.cpg-center-card { padding: 16px; }
|
.cpg-center-card { padding: 16px; }
|
||||||
|
|
||||||
.cpg-center-bar {
|
.cpg-center-bar {
|
||||||
@@ -180,33 +180,69 @@
|
|||||||
.cpg-center-addinput { flex: 1 1 auto; min-width: 0; }
|
.cpg-center-addinput { flex: 1 1 auto; min-width: 0; }
|
||||||
.cpg-center-search { flex: 0 1 240px; min-width: 0; }
|
.cpg-center-search { flex: 0 1 240px; min-width: 0; }
|
||||||
|
|
||||||
.cpg-center-head { margin: 12px 0 8px; }
|
.cpg-center-head { margin: 12px 0 10px; }
|
||||||
|
|
||||||
.cpg-center-grid {
|
/* 멀티컬럼: 한 열을 위→아래로 읽고, 다 차면 다음 열로 */
|
||||||
display: grid;
|
.cpg-center-cols {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
columns: 4 220px;
|
||||||
gap: 6px;
|
column-gap: 20px;
|
||||||
max-height: 70vh; overflow-y: auto;
|
}
|
||||||
padding-right: 4px;
|
.cpg-center-group {
|
||||||
|
break-inside: avoid; page-break-inside: avoid;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
}
|
||||||
|
.cpg-center-gtitle {
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
padding: 0 4px 4px;
|
||||||
|
border-bottom: 2px solid var(--color-rich-black);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.cpg-center-gname { font-size: 13px; font-weight: 700; letter-spacing: .02em; }
|
||||||
|
.cpg-center-gcount {
|
||||||
|
font-size: 11px; font-weight: 600; line-height: 1;
|
||||||
|
padding: 2px 6px; border-radius: 999px;
|
||||||
|
background: var(--color-subtle-ash); color: var(--color-midtone-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cpg-center-row {
|
.cpg-center-row {
|
||||||
display: flex; align-items: center; gap: 6px;
|
display: flex; align-items: center; gap: 7px;
|
||||||
margin: 0; padding: 7px 10px;
|
padding: 5px 4px;
|
||||||
border: 1px solid var(--color-subtle-ash); border-radius: 10px;
|
border-bottom: 1px solid var(--color-subtle-ash);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
.cpg-center-row:last-child { border-bottom: none; }
|
||||||
.cpg-center-dot {
|
.cpg-center-dot {
|
||||||
flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%;
|
flex: 0 0 auto; width: 7px; height: 7px; border-radius: 50%;
|
||||||
background: var(--color-success-green, #10c22b);
|
background: var(--color-success-green, #10c22b);
|
||||||
}
|
}
|
||||||
.cpg-center-row.is-inactive { opacity: .55; }
|
.cpg-center-row.is-inactive { opacity: .5; }
|
||||||
.cpg-center-row.is-inactive .cpg-center-dot { background: var(--color-subtle-ash); }
|
.cpg-center-row.is-inactive .cpg-center-dot { background: var(--color-subtle-ash); }
|
||||||
.cpg-center-name {
|
.cpg-center-name {
|
||||||
flex: 1 1 auto; min-width: 0;
|
flex: 1 1 auto; min-width: 0;
|
||||||
font-size: 14px; font-weight: 500;
|
font-size: 14px; font-weight: 500;
|
||||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 행 액션: 평소엔 흐리게, hover/focus 시 또렷하게 */
|
||||||
|
.cpg-center-acts {
|
||||||
|
display: inline-flex; gap: 2px; flex: 0 0 auto;
|
||||||
|
opacity: 0; transition: opacity .12s ease;
|
||||||
|
}
|
||||||
|
.cpg-center-row:hover .cpg-center-acts,
|
||||||
|
.cpg-center-row:focus-within .cpg-center-acts { opacity: 1; }
|
||||||
|
@media (hover: none) {
|
||||||
|
.cpg-center-acts { opacity: .7; }
|
||||||
|
}
|
||||||
|
.cpg-icon-btn {
|
||||||
|
border: 0; background: none; cursor: pointer;
|
||||||
|
font-size: 11px; line-height: 1; padding: 3px 5px; border-radius: 6px;
|
||||||
|
color: var(--color-midtone-gray);
|
||||||
|
}
|
||||||
|
.cpg-icon-btn:hover { background: var(--color-subtle-ash); color: var(--color-rich-black); }
|
||||||
|
.cpg-icon-btn.is-danger:hover { background: var(--color-ghost-gray); color: var(--color-callout-red); }
|
||||||
|
|
||||||
|
.cpg-center-empty { margin: 8px 0 0; }
|
||||||
|
|
||||||
.cpg-btn-sm { padding: 3px 8px; font-size: 12px; flex: 0 0 auto; }
|
.cpg-btn-sm { padding: 3px 8px; font-size: 12px; flex: 0 0 auto; }
|
||||||
|
|
||||||
/* ── 라인 테이블 ── */
|
/* ── 라인 테이블 ── */
|
||||||
|
|||||||
Reference in New Issue
Block a user