fix(M01): 식 해석기 흠 셋 — 단위 없는 맨 수 · 「식 = 이름」 꼴 · 비목 안 고른 덧줄

· 단위 칸에 「없음」(맨수 · - · % · 빈 글)을 골라 무차원으로 둘 수 있게 함 —
  횟수·배율을 「회」 로 두어 「원/회」 가 남던 덧셈 검사 걸림을 풂 ·
  같은 단위끼리 나누면 맨 수가 되는 것도 맞음
· 줄을 「이름 = 식」 과 「식 = 이름」(A * B = C) 둘 다 받음 — 한쪽이 값 이름 하나면 그쪽이 이름 ·
  양쪽이 다 식이면 그 줄의 등호 자리를 짚음 · 괄호 안 등호와 비교 기호는 안 가름
· 비목 안 고른 덧줄을 「못 옮김」 으로 막지 않고 그 값의 「정할것」 에 「비목」 을 얹어 짚어 줌 ·
  돈 아닌 로직은 덧줄이 없어 안 물음
· 계약 _화면_계약.md 8장(다른 창이 7장을 먼저 씀) · 시험 test_m01_draft.py 18개

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1MKKZKpUHTPKb513FneU8
This commit is contained in:
2026-09-21 20:57:43 +09:00
co-authored by Claude Opus 5
parent 45746c9ed7
commit 8a91fa3cca
4 changed files with 168 additions and 35 deletions
+2 -2
View File
@@ -278,14 +278,14 @@ def logic_draft(
save: bool = False,
owner: str = OWNERS[0],
) -> dict:
"""줄로 적은 식 → 변수 · 검사(`_화면_계약.md` 7장).
"""줄로 적은 식 → 변수 · 검사(`_화면_계약.md` 8장).
`head`(이름 · 결과단위 …)가 오면 로직 한 줄까지 미리 보이고, `save` 면 다 정해진 때만
자체 로직으로 저장(키 GX 는 서버) · 덜 정했거나 걸리면 422 로 아무것도 안 씀.
"""
import master_draft as draft # store 가 scripts 를 길에 올린 뒤라야 함
got = draft.read(text, decided)
got = draft.read(text, decided, head)
if head is None:
return {**got, "logic": None}
row, bad = draft.옮기기(text, decided, head)
+6 -4
View File
@@ -140,16 +140,18 @@
`POST /logic/draft` 받음 `{text, decided, head?, save?, owner?}` — 줄로 적은 식을 받아 변수를 뽑고 검사한 뒤 로직 한 줄로 옮김(해석기 `scripts/master_draft.py`).
- `text` = 줄마다 「이름 = 식」 · 빈 줄과 `#` 는 건너뜀 · **마지막 줄 왼쪽이 결과 변수**. 식 글자는 `_틀.md` 8장 범위만.
- `text` = 줄마다 「이름 = 식」 **또는 「식 = 이름」**(`A * B = C`) · 한쪽이 값 이름 하나면 그쪽이 이름 · 양쪽이 다 식이면 그 줄의 등호 자리를 짚어 `못읽음`. 괄호 안 등호(`찾기(…, 뒷길이=높이)`)와 비교 기호(`==` `<=` `>=` `!=`)는 안 가름. 빈 줄과 `#` 는 건너뜀 · **마지막 줄의 값 이름이 결과 변수**. 식 글자는 `_틀.md` 8장 범위만.
- `decided` = 변수마다 정한 것 `{이름: {무엇, 단위, 비목, 요소 | 찾기 | 값, 설명 · 고르기 · 범위}}`.
- `무엇``마스터요소`(키 또는 재료 고르기 조건을 `요소` 에 · `비목` 도 정함) · `설계입력`(입력 칸이 됨) · `고정값`(`값` 을 중간으로) · `표찾기`(`찾기` = `{표, 값칸, 조건}``찾기(…).값칸` 을 중간으로).
- `단위` — 마스터요소는 그 줄의 단위(「인」 · 「포」)를 적음. 식에서는 단가라 `원/단위` 로 셈.
- **단위 없는 맨 수**(횟수 · 배율 · 비율)는 단위 칸에 `없음`(또는 `맨수` · `-` · `%` · 빈 글)을 골라 무차원으로 둠 — 「전용 4회」 를 「회」 로 두면 `원/회` 가 남아 덧셈 검사에 걸림. 칸 자체가 없으면 아직 안 정한 것(`정할것``단위`).
- 비목 합 이름(`노무비` · `재료비` · `경비` · `계`)은 정하지 않음 — 덧줄에서만 쓰는 이름.
- `head` = 로직 머리(`이름` · `결과단위` · `원문번호` · `출처` · `비고` · `끝수`). 없으면 검사만 함.
-`{줄, 변수, 결과, 문제, 다됨, logic}``logic` = 옮긴 로직 한 줄(`head` 가 있을 때만 · 저장 전).
- `변수` = `{이름, 처음, 갈래, 정의줄, 쓰임, 무엇, 단위, 비목, 정할것}` · `갈래` = `정의됨`(왼쪽에 나옴) · `미정`(쓰기만 함) · `비목합` · `정할것` = 아직 안 정한 칸 이름들.
- `문제` = `{줄, 자리, 갈래, 말}` · `자리` = 그 줄의 글자 자리(모르면 null) · `갈래` = `못읽음` · `이름` · `두번정의` · `돌고돎` · `안쓰임`(= 결과가 하나인지) · `단위` · `덜정함` · `항` · `비목합`.
- 단위 검사 — 곱셈은 단위를 곱하고 나눗셈은 나눔 · 덧셈·뺄셈과 `만약`·`최소`·`최대`·비교는 단위가 같아야 함. 「m \* m」 과 「㎡」 는 같게 봄. 모르는 단위는 안 따짐.
- `변수` = `{이름, 처음, 갈래, 정의줄, 쓰임, 무엇, 단위, 비목, 정할것}` · `갈래` = `정의됨`(식이 있음) · `미정`(쓰기만 함) · `비목합` · `정할것` = 아직 안 정한 칸 이름들. `단위` 는 그 값이 **식에서** 가지는 단위(무차원은 빈 글).
- `문제` = `{줄, 자리, 갈래, 말}` · `자리` = 그 줄의 글자 자리(모르면 null) · `갈래` = `못읽음` · `이름` · `두번정의` · `돌고돎` · `안쓰임`(= 결과가 하나인지) · `단위` · `덜정함` · `항` · `비목` · `비목합`.
- 단위 검사 — 곱셈은 단위를 곱하고 나눗셈은 나눔(같은 단위끼리 나누면 맨 수) · 덧셈·뺄셈과 `만약`·`최소`·`최대`는 단위가 같아야 함 · 비교는 한쪽이 맨 수면 안 따짐(`높이 <= 3`). 「m \* m」 과 「㎡」 는 같게 봄. 모르는 단위는 안 따짐.
- **덧줄이 될 값**(결과 식의 항 가운데 마스터요소가 없는 것)은 막지 않고 그 값의 `정할것``비목` 을 얹음 — 화면은 「비목을 골라 주세요」 로 보임.
- `save` = true 면 `POST /logic/new` 와 같은 자리에 저장(키 `GX` 는 서버 · 소유 `owner`) → 답에 `{file, version, key, logic}` 이 더 옴. 문제가 하나라도 있으면 422 `{detail: {errors, 문제}}` 로 아무것도 안 씀.
- 옮기는 법 — 돈 로직(`결과단위` 가 「원」 으로 시작)은 결과 식을 `+` 로 갈라 항마다: 마스터요소 변수 하나면 **호표 줄**(나머지 곱이 `수량`) · 비목만 정한 변수면 **덧줄** · 그 밖은 문제 `항`. 나머지 정의 줄은 `중간`(쓰는 차례대로). 돈 아닌 로직은 호표 없이 `중간` + `결과` 식.
- 표 찾기는 글자로 치지 않음 — 변수 하나로 두고 `무엇``표찾기` 로 정할 때 모달이 `찾기` 묶음을 채움.
+96 -29
View File
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
"""줄로 적은 식 → 변수 뽑기 · 검사 · 단위 맞춤 · 로직 틀로 옮기기 (`_화면_계약.md` 7장).
"""줄로 적은 식 → 변수 뽑기 · 검사 · 단위 맞춤 · 로직 틀로 옮기기 (`_화면_계약.md` 8장).
수량 = 길이 * 폭
자재비 = 수량 * 시멘트
· 줄마다 「이름 = 식」 · 빈 줄과 `#` 는 건너뜀 · 마지막 줄 왼쪽이 결과 변수
· 줄마다 「이름 = 식」 또는 「식 = 이름」 · 빈 줄과 `#` 는 건너뜀 · 마지막 줄의 값 이름이 결과 변수
· 식 글자는 `_틀.md` 8장 범위만 — 읽기는 `master_formula` 가 함
· 표 찾기는 글자로 치지 않음 — 변수 하나로 두고 「무엇」 을 `표찾기` 로 정할 때 이음
· 값은 안 씀 — 검사만 하고 `옮기기` 가 로직 한 줄을 만듦(저장은 `Store_Make.logic_new`)
@@ -32,8 +32,9 @@ ALIAS = {
"M": ("m", 1),
}
SQUARED = {2: "", 3: ""}
DIMLESS = ("%", "") # 비율은 단위가 없음 — 「1 + 증가율 / 100」 이 안 걸리게
_ASSIGN = re.compile(r"^\s*([\w가-힣]+)\s*=(?!=)\s*(.*)$")
# 단위가 없는 맨 수 — 횟수 · 배율 · 비율. 빈 글도 같음(「없음」 을 고른 것과 같게 봄)
DIMLESS = ("%", "", "없음", "맨수", "맨 수", "-")
_NAME_ONLY = re.compile(r"^[\w가-힣]+$")
# ── 자리를 적어 두는 읽기 ──────────────────────────────────────────────
@@ -224,18 +225,51 @@ def find_text(pick: dict) -> str:
# ── 읽기 · 검사 ───────────────────────────────────────────────────────
def _is_name(text: str) -> bool:
"""값 이름 하나 — 맨 수는 이름이 아님."""
return bool(_NAME_ONLY.fullmatch(text)) and not mf._NUMBER.fullmatch(text)
def _split(raw: str) -> int:
"""줄을 가를 등호 자리 — 괄호 안과 비교 기호(`==` `<=` `>=` `!=`)는 건너뜀 · 없으면 -1."""
depth = 0
for i, ch in enumerate(raw):
if ch in "([{":
depth += 1
elif ch in ")]}":
depth -= 1
elif ch == "=" and depth == 0:
if raw[i - 1 : i] in ("<", ">", "!", "=") or raw[i + 1 : i + 2] == "=":
continue
return i
return -1
def _cut(text: str):
"""(줄 번호, 왼쪽 이름, 식, 식이 시작하는 자리) — 빈 줄과 `#` 는 건너뜀."""
"""(줄 번호, 이름, 식, 식이 시작하는 자리, 까닭) — 빈 줄과 `#` 는 건너뜀.
「이름 = 식」 과 「식 = 이름」 을 다 받음 — 한쪽이 값 이름 하나면 그쪽이 이름.
"""
for at, raw in enumerate(str(text).split("\n"), 1):
if not raw.strip() or raw.lstrip().startswith("#"):
continue
m = _ASSIGN.match(raw)
yield (
at,
(m.group(1) if m else ""),
(m.group(2).strip() if m else raw.strip()),
(m.start(2) if m else len(raw) - len(raw.lstrip())),
)
cut = _split(raw)
if cut < 0:
yield (
at,
"",
raw.strip(),
len(raw) - len(raw.lstrip()),
"「이름 = 식」 이 아님 — 등호가 없음",
)
continue
left, right = raw[:cut], raw[cut + 1 :]
if _is_name(left.strip()):
yield at, left.strip(), right.strip(), cut + 1 + len(right) - len(right.lstrip()), ""
elif _is_name(right.strip()): # 「A * B = C」 — 오른쪽이 이름
yield at, right.strip(), left.strip(), len(left) - len(left.lstrip()), ""
else:
yield at, "", raw.strip(), cut, "양쪽이 다 식 — 한쪽은 값 이름 하나여야 함"
def _order(defined: dict, deps: dict) -> list[str]:
@@ -255,14 +289,14 @@ def _order(defined: dict, deps: dict) -> list[str]:
return out
def _scan(text: str, 정함: dict | None = None) -> dict:
def _scan(text: str, 정함: dict | None = None, 머리: dict | None = None) -> dict:
"""줄 가르기 · 변수 · 검사 · 단위 — `read` 와 `옮기기` 가 같이 씀(나무를 들고 있음)."""
정함 = 정함 or {}
lines, 문제 = [], []
for at, name, body, head in _cut(text):
for at, name, body, head, why in _cut(text):
one = {"": at, "이름": name, "": body, "": head}
if not name:
문제.append(_trouble(at, head, "못읽음", "「이름 = 식」 이 아님"))
if why:
문제.append(_trouble(at, head, "못읽음", why))
elif not body:
문제.append(_trouble(at, head, "못읽음", f"{name}」 오른쪽이 빔"))
else:
@@ -343,6 +377,7 @@ def _scan(text: str, 정함: dict | None = None) -> dict:
got = _unit(one["나무"], units, one[""], one[""], 문제)
units[name] = pinned if pinned is not None else got
덧줄후보 = _extra_names(by, 결과, 정함, 머리) if 결과 in by and 결과 not in loops else set()
변수 = []
for name in sorted(
set(쓰임) | set(정의), key=lambda n: (min([쓰임.get(n, [10**9])[0], 정의.get(n, 10**9)]), n)
@@ -359,16 +394,18 @@ def _scan(text: str, 정함: dict | None = None) -> dict:
"무엇": spec.get("무엇") if 갈래 == "미정" else None,
"단위": unit_text(units.get(name)),
"비목": spec.get("비목"),
"정할것": _todo(갈래, spec, units.get(name)),
"정할것": _todo(갈래, spec, units.get(name), name in 덧줄후보),
}
)
return {"": lines, "변수": 변수, "결과": 결과, "문제": 문제, "차례": order, "정의": 정의}
def _todo(갈래: str, spec: dict, unit) -> list[str]:
def _todo(갈래: str, spec: dict, unit, 덧줄: bool = False) -> list[str]:
"""그 변수에 아직 안 정한 것 — 무엇 · 요소·찾기·값 · 단위 · 비목."""
if 갈래 != "미정":
if 갈래 == "비목합":
return []
if 갈래 == "정의됨": # 식이 있는 값 — 덧줄이 될 항이면 비목만 고르면 됨
return ["비목"] if 덧줄 and spec.get("비목") not in COST_ITEMS else []
what, need = spec.get("무엇"), []
if what not in WHAT:
need.append("무엇")
@@ -386,9 +423,9 @@ def _todo(갈래: str, spec: dict, unit) -> list[str]:
return need
def read(text: str, 정함: dict | None = None) -> dict:
def read(text: str, 정함: dict | None = None, 머리: dict | None = None) -> dict:
"""줄로 적은 식 → `{줄, 변수, 결과, 문제, 다됨}` — 화면이 그대로 받는 모양(나무는 뺌)."""
got = _scan(text, 정함)
got = _scan(text, 정함, 머리)
다됨 = not got["문제"] and not any(v["정할것"] for v in got["변수"])
return {
"": [{k: v for k, v in one.items() if k != "나무"} for one in got[""]],
@@ -408,22 +445,40 @@ def _factors(node) -> list:
return _factors(node[2]) + _factors(node[3]) if node[0] == "bin" and node[1] == "*" else [node]
def _spread(node, by: dict, 정함: dict, : set) -> list:
"""결과 항 — 항이 이름 하나이고 비목을 안 정했으면 그 식으로 펼침.
def _spread(node, by: dict, 정함: dict, : set, 뿌리: str = "") -> list:
"""결과 항 — (항, 펼치기 전 이름). 항이 이름 하나이고 비목을 안 정했으면 그 식으로 펼침.
호표 줄을 항 안에서 찾으려는 것.
호표 줄을 항 안에서 찾으려는 것 · 못 펼친 항의 `뿌리` 가 덧줄이 될 값 이름.
"""
out = []
for term in _terms(node):
name = term[1] if term[0] == "name" else ""
if name in by and name not in and not (정함.get(name) or {}).get("비목"):
.add(name)
out += _spread(by[name]["나무"], by, 정함, )
out += _spread(by[name]["나무"], by, 정함, , name)
else:
out.append(term)
out.append((term, 뿌리))
return out
def _has_element(term, 정함: dict) -> bool:
return any(
f[0] == "name" and (정함.get(f[1]) or {}).get("무엇") == "마스터요소"
for f in _factors(term)
)
def _extra_names(by: dict, 결과: str, 정함: dict, 머리: dict | None) -> set[str]:
"""결과 식의 항 가운데 마스터요소가 없어 덧줄이 될 값 이름 — 비목을 골라야 함."""
if 머리 is not None and not str(머리.get("결과단위") or "").startswith(""):
return set() # 돈 아닌 로직은 덧줄이 없음
return {
뿌리
for term, 뿌리 in _spread(by[결과]["나무"], by, 정함, set())
if 뿌리 and not _has_element(term, 정함)
}
def _kind_of(요소) -> str:
if isinstance(요소, dict):
return "재료"
@@ -447,13 +502,16 @@ def 옮기기(text: str, 정함: dict | None = None, 머리: dict | None = None)
돈 아닌 로직은 호표 없이 `중간` + `결과` 식.
"""
정함, 머리 = 정함 or {}, dict(머리 or {})
got = _scan(text, 정함)
got = _scan(text, 정함, 머리)
문제 = list(got["문제"])
for v in got["변수"]:
if v["정할것"]:
문제.append(
_trouble(
v["처음"], 0, "덜정함", f"{v['이름']}」 에 {' · '.join(v['정할것'])} 안 정함"
v["처음"],
0,
"덜정함",
f"{v['이름']}」 에 아직 안 정한 것 — {' · '.join(v['정할것'])}",
)
)
if 문제:
@@ -475,7 +533,7 @@ def 옮기기(text: str, 정함: dict | None = None, 머리: dict | None = None)
덧줄이름, = set(), set()
if :
for term in _spread(by[결과]["나무"], by, 정함, ):
for term, 뿌리 in _spread(by[결과]["나무"], by, 정함, ):
facts = _factors(term)
picks = [
f
@@ -520,6 +578,15 @@ def 옮기기(text: str, 정함: dict | None = None, 머리: dict | None = None)
{"이름": name, "": _expr(by[name]["나무"]), "비목": 정함[name]["비목"]}
)
덧줄이름.add(name)
elif 뿌리 in by: # 덧줄이 될 값 — 막지 않고 비목만 짚음
문제.append(
_trouble(
by[뿌리][""],
0,
"비목",
f"덧줄 「{뿌리}」 의 비목을 골라 주세요 — {' · '.join(COST_ITEMS)}",
)
)
else:
문제.append(
_trouble(
+64
View File
@@ -133,6 +133,31 @@ def test_만약과_최소는_갈래마다_단위가_같아야_한다() -> None:
assert md.read("값 = 최소(길이, 길이 * 2)", 정함)["문제"] == []
def test_단위_없음을_골라_맨_수로_다룬다() -> None:
"""횟수 · 배율 — 단위 칸에 「없음」 을 고르면 무차원(3-3 화면 흠 ①)."""
= {"금액": {"무엇": "설계입력", "단위": ""}, "전용": {"무엇": "설계입력", "단위": ""}}
got = md.read("몫 = 금액 / 전용\n계금액 = 몫 + 금액", )
assert _var(got, "")["단위"] == "원/회" # 「회」 로 두면 덧셈에서 걸림
assert [x["갈래"] for x in got["문제"]] == ["단위"]
맨수 = dict(, 전용={"무엇": "설계입력", "단위": "없음"})
깨끗 = md.read("몫 = 금액 / 전용\n계금액 = 몫 + 금액", 맨수)
assert _var(깨끗, "")["단위"] == "" and 깨끗["문제"] == []
assert _var(깨끗, "전용")["단위"] == "" and _var(깨끗, "전용")["정할것"] == []
for in ("", "맨수", "-", "%"):
= dict(, 전용={"무엇": "설계입력", "단위": })
assert md.read("몫 = 금액 / 전용\n계금액 = 몫 + 금액", )["문제"] == [],
없으면 = dict(, 전용={"무엇": "설계입력"}) # 단위 칸 자체가 없으면 아직 안 정한 것
assert _var(md.read("몫 = 금액 / 전용", 없으면), "전용")["정할것"] == ["단위"]
def test_나눗셈으로_단위가_없어진다() -> None:
정함 = {"길이": {"무엇": "설계입력", "단위": "m"}, "넓이": {"무엇": "설계입력", "단위": ""}}
assert _var(md.read("폭 = 넓이 / 길이", 정함), "")["단위"] == "m"
assert _var(md.read("비 = 길이 / 길이", 정함), "")["단위"] == "" # 같은 단위끼리 나누면 맨 수
assert md.read("합 = 길이 / 길이 + 2", 정함)["문제"] == []
# ── ③ 그 밖 검사 ──────────────────────────────────────────────────────
def test_서로_물고_도는_식을_잡는다() -> None:
kinds = [x["갈래"] for x in md.read(LOOP)["문제"]]
@@ -152,6 +177,29 @@ def test_두_번_정의_안_쓰임_결과_하나() -> None:
assert md.read("이건 식이 아님")["문제"][0]["갈래"] == "못읽음"
def test_식이_왼쪽에_와도_받는다() -> None:
"""「A * B = C」 로 적는 사용자도 있음(3-3 화면 흠 ②)."""
정함 = {"길이": {"무엇": "설계입력", "단위": "m"}, "": {"무엇": "설계입력", "단위": "m"}}
got = md.read("길이 * 폭 = 넓이", 정함)
assert got["문제"] == [] and got["결과"] == "넓이"
assert _var(got, "넓이")["갈래"] == "정의됨" and _var(got, "넓이")["단위"] == ""
assert got[""][0][""] == "길이 * 폭"
assert md.read("넓이 = 길이 * 폭", 정함)["변수"] == got["변수"] # 어느 쪽으로 적든 같음
양쪽 = md.read("길이 * 폭 = 넓이 * 2", 정함)["문제"]
assert 양쪽[0]["갈래"] == "못읽음" and "양쪽이 다 식" in 양쪽[0][""]
assert 양쪽[0]["자리"] == "길이 * 폭 = 넓이 * 2".index("=")
assert md.read("길이 * 폭 = 4", 정함)["문제"][0]["갈래"] == "못읽음" # 맨 수는 이름이 아님
def test_비교_기호와_찾기_안_등호는_안_가른다() -> None:
정함 = {"높이": {"무엇": "설계입력", "단위": "m"}}
assert md.read("값 = 만약(높이 <= 3, 1, 2)", 정함)["문제"] == []
assert md.read("값 = 만약(높이 == 3, 1, 2)", 정함)["문제"] == []
안쪽 = md.read("품 = 찾기(QF000421, 뒷길이=높이).석공", 정함)
assert 안쪽["문제"] == [] and 안쪽[""][0]["이름"] == ""
# ── ⑤ 로직 틀로 옮기기 ────────────────────────────────────────────────
def test_다_정하면_로직_틀로_옮긴다() -> None:
row, bad = md.옮기기(GOOD, GOOD_정함, GOOD_머리)
@@ -192,6 +240,22 @@ def test_덜_정했으면_안_옮긴다() -> None:
assert row == {} and any(x["갈래"] == "덜정함" for x in bad)
def test_비목_안_고른_덧줄은_막지_않고_짚어_준다() -> None:
"""「못 옮김」 으로 막지 않고 그 값에 「비목」 을 정할 것으로 얹음(3-3 화면 흠 ③)."""
빼기 = {k: v for k, v in GOOD_정함.items() if k != "잡재료비"}
got = md.read(GOOD, 빼기, GOOD_머리)
assert _var(got, "잡재료비")["정할것"] == ["비목"] and got["다됨"] is False
assert [x["갈래"] for x in got["문제"]] == [] # 검사에는 안 걸림 — 고를 것이 남았을 뿐
row, bad = md.옮기기(GOOD, 빼기, GOOD_머리)
assert row == {} and [x["갈래"] for x in bad] == ["덜정함"]
assert "잡재료비" in bad[0][""] and "비목" in bad[0][""]
assert _var(md.read(GOOD, GOOD_정함, GOOD_머리), "잡재료비")["정할것"] == []
안돈 = md.read(GOOD, 빼기, {"이름": "수량만", "결과단위": ""}) # 돈 아닌 로직은 덧줄이 없음
assert _var(안돈, "잡재료비")["정할것"] == []
# ── 서버 길 ───────────────────────────────────────────────────────────
def test_서버_길이_변수와_문제를_돌려_준다(client: TestClient) -> None:
got = _post(client, {"text": GOOD, "decided": GOOD_정함})