[chore] solution/backend,frontend: 에이전트 화면 보류 — 설정으로 닫고 코드는 남긴다
카카오톡 채널 개설이 법인폰 본인인증에 걸려 보류됐다. 채널이 없으면 대화창은 사장님에게 **어디에도 닿지 않는 입구**이고, 열려 있으면 "되는 기능" 으로 오해한다. - config/agent_config: AGENT_CHAT_ENABLED 신설(기본 0) - runtime.is_configured(): 스위치와 LLM 키를 둘 다 본다 — 화면을 우회해 API 를 직접 불러도 AGENT_NOT_CONFIGURED 다 - AgentChatDock · KakaoChannelCard: 조건 미충족이면 통째로 감춘다(return null). 연결 카드는 connection_enabled 가 기준이라 설정만 채우면 그대로 다시 나타난다 - ★ 코드를 지우지 않았다 — 되돌릴 때 커밋을 되짚지 않고 값 둘만 채우면 된다 ★ Threads 카드와 판단이 갈린 것이 맞다. 저쪽은 사장님이 곧 쓸 수 있는 기능이라 자리를 두고 버튼만 죽였고, 이쪽은 언제 열릴지 말해 줄 수 없다. test_agent_runtime(스위치 2건 추가)·test_kakao_link 34 passed. npm run lint 통과 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
59df616a7b
commit
a553e41197
@ -83,6 +83,9 @@ ALIMTALK_TEMPLATE_CODE=
|
||||
# 묶어야 한다. 빌더에서 코드를 받아 채널에 한 번 입력하는 절차다.
|
||||
# ★ 이 값이 비면 연결 화면이 아예 안 뜬다 — 어디에 코드를 칠지 말해 줄 수 없는데
|
||||
# 코드만 발급하면 사장님에게는 고장난 화면이다.
|
||||
# ★ 사장님 대화창(에이전트). 기본 꺼짐 — 카카오톡 채널이 준비되기 전에는 띄우지 않는다.
|
||||
# 코드는 다 있지만 채널 없이 열어 두면 사장님에게는 어디에도 닿지 않는 입구다.
|
||||
AGENT_CHAT_ENABLED=0
|
||||
KAKAO_CHANNEL_PUBLIC_ID=
|
||||
KAKAO_LINK_CODE_TTL_MIN=10
|
||||
KAKAO_LINK_MAX_ATTEMPTS=5
|
||||
|
||||
2
.serena/.gitignore
vendored
Normal file
2
.serena/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/cache
|
||||
/project.local.yml
|
||||
169
.serena/project.yml
Normal file
169
.serena/project.yml
Normal file
@ -0,0 +1,169 @@
|
||||
# the name by which the project can be referenced within Serena/when chatting with the LLM.
|
||||
project_name: "o2o-site-AEO"
|
||||
|
||||
# list of language servers to start when using the LSP backend; choose from:
|
||||
# ada al angular ansible bash
|
||||
# bsl clojure cpp cpp_ccls crystal
|
||||
# csharp csharp_omnisharp cue dart deno
|
||||
# elixir elm erlang fortran fsharp
|
||||
# gdscript gleam go groovy haskell
|
||||
# haxe hlsl html java json
|
||||
# julia kotlin latex lean4 lua
|
||||
# luau markdown matlab msl nextflow
|
||||
# nix ocaml pascal perl php
|
||||
# php_phpactor php_phpantom powershell python python_basedpyright
|
||||
# python_jedi python_pyrefly python_ty qml r
|
||||
# rego ruby ruby_solargraph rust scala
|
||||
# scss solidity svelte swift systemverilog
|
||||
# terraform toml typescript typescript_vts vue
|
||||
# wolfram yaml zig
|
||||
# (This list may be outdated; generated with scripts/print_language_list.py;
|
||||
# For the current list, see values of the LanguageServerId enum here:
|
||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
|
||||
# For some languages, there are several alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||
# Note:
|
||||
# - For C, use cpp
|
||||
# - For JavaScript, use typescript
|
||||
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
|
||||
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
|
||||
# - For Deno projects, use deno (serves the same .ts/.js files as typescript; requires the deno CLI on PATH)
|
||||
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
|
||||
# - For Free Pascal/Lazarus, use pascal
|
||||
# Special requirements:
|
||||
# Some language servers require additional setup/installations.
|
||||
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
|
||||
# When using multiple language servers, the first language server that supports a given file will be used for that file.
|
||||
# The first language server is the default language and the respective language server will be used as a fallback.
|
||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||
language_servers:
|
||||
- typescript
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: "utf-8"
|
||||
|
||||
# optional shell command to run before the language backend (LSP or JetBrains) is initialised.
|
||||
# the command runs in the project root directory and is only executed if the project is trusted
|
||||
# (see trusted_project_path_patterns in the global configuration).
|
||||
# serena waits for the command to exit: a non-zero exit code is logged as an error but does not
|
||||
# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety
|
||||
# backstop for non-terminating commands; on expiry the process is killed and activation continues.
|
||||
# example: activation_command: "npx nx run-many -t build"
|
||||
activation_command:
|
||||
|
||||
# maximum time in seconds to wait for activation_command to complete before killing it (default 180s).
|
||||
# must be a positive number.
|
||||
activation_command_timeout: 180.0
|
||||
|
||||
# line ending convention to use when writing source files.
|
||||
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
|
||||
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
|
||||
line_ending:
|
||||
|
||||
# The language backend to use for this project.
|
||||
# If not set, the global setting from serena_config.yml is used.
|
||||
# Valid values: LSP, JetBrains
|
||||
# Note: the backend is fixed at startup. If a project with a different backend
|
||||
# is activated post-init, an error will be returned.
|
||||
language_backend:
|
||||
|
||||
# whether to use project's .gitignore files to ignore files
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
# advanced configuration option allowing to configure language server-specific options.
|
||||
# Maps the language key to the options.
|
||||
# The settings are considered only if the project is trusted (see global configuration to define trusted projects).
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings
|
||||
ls_specific_settings: {}
|
||||
|
||||
# list of workspace folder paths (LSP backend only).
|
||||
# These folders will be used to build up Serena's symbol index.
|
||||
# Paths must be within the project root and should thus be relative to the project root.
|
||||
# Furthermore, the paths should not be filtered by ignore settings.
|
||||
# Default setting: The entire project root folder (".") is considered.
|
||||
# In (large) monorepos, this can be used to index only subfolders of the project root, e.g.
|
||||
# ls_workspace_folders:
|
||||
# - "./subproject1"
|
||||
# - "./subproject2"
|
||||
ls_workspace_folders:
|
||||
- "."
|
||||
|
||||
# list of additional workspace folder paths for cross-package reference support.
|
||||
# Paths can be absolute or relative to the project root.
|
||||
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
|
||||
# symbols and references across package boundaries, but these folders are not indexed by Serena,
|
||||
# i.e. the respective symbols will not be found using Serena's symbol search tools.
|
||||
# Example:
|
||||
# additional_workspace_folders:
|
||||
# - ../sibling-package
|
||||
# - ../shared-lib
|
||||
ls_additional_workspace_folders: []
|
||||
|
||||
# list of additional paths to ignore in this project.
|
||||
# Same syntax as gitignore, so you can use * and **.
|
||||
# Important: quote patterns that start with `*`, otherwise YAML treats them as aliases.
|
||||
# Example:
|
||||
# ignored_paths:
|
||||
# - "examples/**"
|
||||
# - ".worktrees/**"
|
||||
# - "**/bin/**"
|
||||
# - "**/obj/**"
|
||||
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
# list of tool names to exclude.
|
||||
# This extends the existing exclusions (e.g. from the global configuration)
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
excluded_tools: []
|
||||
|
||||
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
|
||||
# This extends the existing inclusions (e.g. from the global configuration).
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
included_optional_tools: []
|
||||
|
||||
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
||||
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
fixed_tools: []
|
||||
|
||||
# list of mode names that are to be activated by default, overriding the setting in the global configuration.
|
||||
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
|
||||
# If the setting is undefined/empty, the default_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
||||
# Therefore, you can set this to [] if you do not want the default modes defined in the global config to apply
|
||||
# for this project.
|
||||
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||
default_modes:
|
||||
|
||||
# list of mode names to be activated additionally for this project, e.g. ["query-projects"]
|
||||
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||
added_modes:
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
# time budget (seconds) per tool call for the retrieval of additional symbol information
|
||||
# such as docstrings or parameter information.
|
||||
# This overrides the corresponding setting in the global configuration; see the documentation there.
|
||||
# If null or missing, use the setting from the global configuration.
|
||||
symbol_info_budget:
|
||||
|
||||
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
read_only_memory_patterns: []
|
||||
|
||||
# list of regex patterns for memories to completely ignore.
|
||||
# Matching memories will not appear in list_memories or activate_project output
|
||||
# and cannot be accessed via read_memory or write_memory.
|
||||
# To access ignored memory files, use the read_file tool on the raw file path.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
# Example: ["_archive/.*", "_episodes/.*"]
|
||||
ignored_memory_patterns: []
|
||||
@ -169,6 +169,9 @@
|
||||
`services/*` 를 통과한다. `collect_service.store_facts` 가 크롤러에 걸어 둔 그 문이다.
|
||||
- **카카오 채널 발화자는 우리 `user_id` 가 아니다** — 채널 단위 익명 키다.
|
||||
`owner_kakao_links` 매핑 없이 발화자를 믿으면 **채널 진입점만 소유자 범위 밖**에 놓인다.
|
||||
- **에이전트 화면은 2026-09-21 기준 감춰져 있다** — `AGENT_CHAT_ENABLED=0`(기본) ·
|
||||
`KAKAO_CHANNEL_PUBLIC_ID` 빔. 카카오톡 채널이 보류돼서이고 **코드는 멀쩡하다**.
|
||||
"기능이 없다" 고 판단해 지우지 않는다([AGENT.md](docs/AGENT.md)).
|
||||
- **에이전트 등급을 모델이 정하게 두지 않는다** — 확인이 필요한 행위인지는 `services/agent/tools.py`
|
||||
레지스트리가 못 박는다. 응답 스키마에 그 칸을 만들면 프롬프트에 끼어든 한 줄이 확인 절차를 건너뛴다.
|
||||
- **실행 결과 문구를 LLM 이 쓰게 두지 않는다** — 모델은 **하지 않은 일을 했다고 말할 수 있고**,
|
||||
|
||||
@ -7,6 +7,23 @@
|
||||
지금까지 만든 것은 **1단계(신원 연결)** 와 **2단계(도구·런타임·빌더 채팅창)** 다.
|
||||
카카오 채널 웹훅은 아직 없다.
|
||||
|
||||
## ★ 지금은 화면에서 감춰져 있다 (2026-09-21 보류)
|
||||
|
||||
카카오톡 채널 개설이 **법인폰 본인인증**에 걸려 보류됐다. 채널이 없으면 이 기능은
|
||||
사장님에게 **어디에도 닿지 않는 입구**다 — 열어 두면 "되는 기능" 으로 오해한다.
|
||||
|
||||
| 화면 | 감추는 조건 |
|
||||
|---|---|
|
||||
| 대화창(`AgentChatDock`) | `AGENT_CHAT_ENABLED=0` (기본값) |
|
||||
| 연결 카드(`KakaoChannelCard`) | `KAKAO_CHANNEL_PUBLIC_ID` 가 빔 (기본값) |
|
||||
|
||||
**코드는 그대로 두고 설정으로만 닫았다.** 채널이 준비되면 값 둘을 채우고 다시 띄우면 된다 —
|
||||
되돌릴 때 커밋을 되짚지 않는다. 서버도 함께 닫힌다(`runtime.is_configured()` 가 스위치를
|
||||
보므로, 화면을 우회해 API 를 직접 불러도 `AGENT_NOT_CONFIGURED` 다).
|
||||
|
||||
★ Threads 카드는 반대로 '자리는 두고 버튼만 죽이는' 쪽이다. 저쪽은 사장님이 **곧 쓸 수 있는**
|
||||
기능이라 존재를 알려야 했고, 이쪽은 언제 열릴지 말해 줄 수 없다. 판단이 갈린 이유가 그것이다.
|
||||
|
||||
## 왜 신원 연결이 먼저인가
|
||||
|
||||
카카오 채널이 주는 발화자 식별자는 **채널 단위 익명 키**다. 우리 `user_id` 와 아무 관계가 없다.
|
||||
|
||||
@ -1,5 +1,23 @@
|
||||
# 개발 일지
|
||||
|
||||
## 2026-09-21 — 에이전트 화면 보류: 설정으로 닫는다(코드는 그대로)
|
||||
|
||||
카카오톡 채널 개설이 **법인폰 본인인증**에 걸려 보류됐다(사장님 지시: "이 작업은 여기서 딱
|
||||
보류하고, 사용못하게 대화 할 수 있는 부분을 숨겨줘"). 채널이 없으면 대화창은 사장님에게
|
||||
**어디에도 닿지 않는 입구**이고, 열려 있으면 "되는 기능" 으로 오해한다.
|
||||
|
||||
- `AGENT_CHAT_ENABLED` 신설(기본 `0`). `runtime.is_configured()` 가 스위치와 LLM 키를 **둘 다**
|
||||
본다 — 화면을 우회해 API 를 직접 불러도 `AGENT_NOT_CONFIGURED` 다.
|
||||
- `AgentChatDock` · `KakaoChannelCard` 둘 다 조건 미충족이면 `return null` 로 통째로 감춘다.
|
||||
연결 카드는 `connection_enabled=false` 가 기준이라 설정을 채우면 그대로 다시 나타난다.
|
||||
- ★ **코드를 지우지 않았다.** 되돌릴 때 커밋을 되짚지 않고 값 둘만 채우면 된다.
|
||||
|
||||
★ Threads 카드와 판단이 갈린 것이 맞다 — 저쪽은 '자리는 두고 버튼만 죽인다'(사장님이 곧 쓸 수
|
||||
있는 기능이라 존재를 알려야 했다), 이쪽은 언제 열릴지 말해 줄 수 없어 감춘다.
|
||||
|
||||
**검증** — `test_agent_runtime`(스위치 테스트 2건 추가)·`test_kakao_link` 34 passed.
|
||||
`npm run lint` 통과.
|
||||
|
||||
## 2026-09-21 — 사장님 에이전트 2단계: 도구 레지스트리 · 런타임 · 빌더 채팅창
|
||||
|
||||
**왜 카카오톡보다 이걸 먼저 만드나**
|
||||
|
||||
@ -23,11 +23,20 @@ class AgentConfig(BaseSettings):
|
||||
# 코드가 짧아서(사람이 손으로 친다) 무차별 대입이 가능하다. 시도 수로 끊는다.
|
||||
KAKAO_LINK_MAX_ATTEMPTS: int = 5
|
||||
|
||||
# ★ 기본 꺼짐. 카카오톡 채널 개설이 법인폰 본인인증에 걸려 보류됐고(2026-09-21),
|
||||
# 채널 없이 대화창만 띄우면 사장님에게는 **어디에도 닿지 않는 입구**가 된다.
|
||||
# 코드는 그대로 두고 이 값으로만 연다 — 되돌릴 때 커밋을 되짚지 않아도 된다.
|
||||
AGENT_CHAT_ENABLED: str = "0"
|
||||
|
||||
|
||||
def get(name, default=""):
|
||||
return getattr(AgentConfig(), name, default) or default
|
||||
|
||||
|
||||
def chat_enabled() -> bool:
|
||||
return get("AGENT_CHAT_ENABLED", "0") == "1"
|
||||
|
||||
|
||||
def kakao_link_enabled() -> bool:
|
||||
return bool(get("KAKAO_CHANNEL_PUBLIC_ID"))
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ from common.enums import DBWRType, ErrorType, PlaceCategory
|
||||
from common.database.db_session_manager import DB_SESSION_MNG
|
||||
from common.database.model.models import places
|
||||
from common.models.gmodel import UserInfo
|
||||
from config import agent_config as config
|
||||
from config.server_configs import external_api_config
|
||||
from crud.fact_crud import FactCRUD
|
||||
from crud.place_crud import PlaceCRUD
|
||||
@ -42,7 +43,14 @@ class AgentError(RuntimeError):
|
||||
|
||||
|
||||
def is_configured() -> bool:
|
||||
return provider.active().is_configured()
|
||||
"""대화창을 열 수 있나 — 스위치와 LLM 키를 **둘 다** 본다.
|
||||
|
||||
★ `AGENT_CHAT_ENABLED` 가 기본 꺼짐이다(config/agent_config). 카카오톡 채널이 준비되기
|
||||
전에는 대화창을 띄우지 않는다 — 코드는 다 있지만 사장님 입장에서는 어디에도 닿지 않는
|
||||
입구이고, 열려 있으면 "되는 기능" 으로 오해한다.
|
||||
이건 Threads 카드처럼 '자리는 두고 버튼만 죽이는' 경우와 다르다. 저쪽은 사장님이
|
||||
**곧 쓸 수 있는** 기능이라 존재를 알려야 했고, 이쪽은 아직 제품이 아니다."""
|
||||
return config.chat_enabled() and provider.active().is_configured()
|
||||
|
||||
|
||||
async def _load_place(user: UserInfo, place_id: str):
|
||||
|
||||
@ -26,6 +26,7 @@ def choose(monkeypatch):
|
||||
def _set(payload):
|
||||
monkeypatch.setattr(runtime, "_choose", AsyncMock(return_value=payload))
|
||||
|
||||
monkeypatch.setenv("AGENT_CHAT_ENABLED", "1")
|
||||
monkeypatch.setattr(runtime, "is_configured", lambda: True)
|
||||
return _set
|
||||
|
||||
@ -222,3 +223,26 @@ def test_읽기_도구는_확인을_요구하지_않는다():
|
||||
assert tools.REGISTRY["set_fact"].grade == ToolGrade.REVERSIBLE
|
||||
assert tools.REGISTRY["publish"].grade == ToolGrade.SEMI
|
||||
assert tools.REGISTRY["publish"].confirm
|
||||
|
||||
|
||||
# ── 보류 스위치 ─────────────────────────────────────────────────────────
|
||||
|
||||
def test_스위치가_꺼져_있으면_키가_있어도_안_열린다(monkeypatch):
|
||||
"""★ 기본이 꺼짐이다. 카카오톡 채널이 준비되기 전에는 대화창을 띄우지 않는다 —
|
||||
코드는 다 있지만 사장님에게는 어디에도 닿지 않는 입구다."""
|
||||
monkeypatch.setattr(runtime.provider, "active", lambda: SimpleNamespace(is_configured=lambda: True))
|
||||
monkeypatch.delenv("AGENT_CHAT_ENABLED", raising=False)
|
||||
assert runtime.is_configured() is False
|
||||
monkeypatch.setenv("AGENT_CHAT_ENABLED", "1")
|
||||
assert runtime.is_configured() is True
|
||||
monkeypatch.setenv("AGENT_CHAT_ENABLED", "0")
|
||||
assert runtime.is_configured() is False
|
||||
|
||||
|
||||
async def test_꺼진_동안_대화_요청은_거절된다(client, auth_headers, monkeypatch, db_engine):
|
||||
monkeypatch.delenv("AGENT_CHAT_ENABLED", raising=False)
|
||||
h, pid = await seed(client, auth_headers)
|
||||
res = await client.post(f"/v1/agent/chat/{pid}", headers=h, json={"message": "안녕"})
|
||||
assert res.status_code == 409
|
||||
assert res.json()["detail"] == "AGENT_NOT_CONFIGURED"
|
||||
assert (await client.get("/v1/agent/status", headers=h)).json()["enabled"] is False
|
||||
|
||||
@ -51,8 +51,13 @@ export function AgentChatDock({sites}: {sites: Site[]}) {
|
||||
endRef.current?.scrollIntoView({behavior: 'smooth'});
|
||||
}, [turns, open]);
|
||||
|
||||
// 가게가 없으면 대화할 대상이 없다. 상태를 못 읽었을 때도 접는다.
|
||||
if (enabled === null || sites.length === 0) return null;
|
||||
// ★ 꺼져 있으면 **통째로 감춘다**(서버 `AGENT_CHAT_ENABLED`, 기본 꺼짐).
|
||||
// 카카오톡 채널이 준비되기 전에는 이 대화창이 어디에도 닿지 않는 입구다 —
|
||||
// 보이면 사장님은 "되는 기능" 으로 오해한다.
|
||||
// ★ Threads 카드와 반대 판단인 것이 맞다. 저쪽은 사장님이 **곧 쓸 수 있는** 기능이라
|
||||
// 자리를 두고 버튼만 죽였고, 이쪽은 아직 제품이 아니다.
|
||||
// 가게가 없을 때와 상태를 못 읽었을 때도 접는다.
|
||||
if (!enabled || sites.length === 0) return null;
|
||||
|
||||
async function send(body: {message?: string; confirm?: {tool: string; args: Record<string, unknown>}}, echo: string) {
|
||||
setTurns((t) => [...t, {who: 'owner', text: echo}]);
|
||||
@ -130,11 +135,6 @@ export function AgentChatDock({sites}: {sites: Site[]}) {
|
||||
<li>· 지금 저장된 정보 보여줘</li>
|
||||
<li>· 내 사이트 발행됐어?</li>
|
||||
</ul>
|
||||
{!enabled && (
|
||||
<p className="mt-3 text-destructive">
|
||||
대화 기능이 아직 켜져 있지 않습니다. 관리자에게 문의해 주세요.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -183,12 +183,12 @@ export function AgentChatDock({sites}: {sites: Site[]}) {
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.nativeEvent.isComposing) ask();
|
||||
}}
|
||||
disabled={busy || !enabled}
|
||||
disabled={busy}
|
||||
maxLength={500}
|
||||
placeholder="체크인 시간 3시로 바꿔줘"
|
||||
className="min-w-0 flex-1 rounded-md border border-border bg-background px-2 py-1.5 text-xs"
|
||||
/>
|
||||
<Button size="sm" variant="primary" disabled={busy || !enabled || !draft.trim()} onClick={ask}>
|
||||
<Button size="sm" variant="primary" disabled={busy || !draft.trim()} onClick={ask}>
|
||||
<Send className="size-4" />
|
||||
<span className="sr-only">보내기</span>
|
||||
</Button>
|
||||
|
||||
@ -16,8 +16,12 @@ import {agentApi, type KakaoLinkCode, type KakaoLinkState} from './api';
|
||||
* "다시 받기" 를 항상 옆에 둔다 — 못 보여주는 것과 잃어버린 것은 다른 상태이고,
|
||||
* 화면이 그 둘을 구별해 말해야 한다.
|
||||
*
|
||||
* ★ 준비 전에도 자리는 보여주고 버튼만 죽인다.
|
||||
* 숨기면 기능이 없는 것처럼 보인다(2026-09-14, Threads 카드에서 실제로 겪었다).
|
||||
* ★ 채널이 없으면 **통째로 감춘다**(`connection_enabled=false`).
|
||||
* Threads 카드는 반대로 '자리는 두고 버튼만 죽이는' 쪽을 골랐는데, 저쪽은 사장님이
|
||||
* **곧 쓸 수 있는** 기능이라 존재를 알려야 했다. 이쪽은 카카오톡 채널 개설이 법인폰
|
||||
* 본인인증에 걸려 보류됐고(2026-09-21), 언제 열릴지 말해 줄 수 없다 —
|
||||
* 그런 카드는 사장님에게 "눌러도 안 되는 버튼" 하나일 뿐이다.
|
||||
* 채널이 준비돼 `KAKAO_CHANNEL_PUBLIC_ID` 를 채우면 이 카드가 그대로 다시 나타난다.
|
||||
*/
|
||||
export function KakaoChannelCard() {
|
||||
const [state, setState] = useState<KakaoLinkState | null>(null);
|
||||
@ -38,10 +42,9 @@ export function KakaoChannelCard() {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
// 상태를 아직 못 읽었을 때만 접는다(로그인 직후 한순간). '준비 안 됨' 과는 다르다.
|
||||
if (!state) return null;
|
||||
// 상태를 못 읽었거나(로그인 직후 한순간) 채널이 준비되지 않았으면 접는다(위 주석).
|
||||
if (!state || !state.connection_enabled) return null;
|
||||
|
||||
const ready = state.connection_enabled;
|
||||
const linked = state.status === 'LINKED';
|
||||
const channelUrl = issued?.channel_url || state.channel_url;
|
||||
|
||||
@ -75,17 +78,15 @@ export function KakaoChannelCard() {
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-sm font-bold">카카오톡으로 관리 · 채널 연결</h2>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{!ready
|
||||
? '채널을 준비하고 있습니다. 열리면 여기서 연결합니다.'
|
||||
: linked
|
||||
? '연결되어 있습니다. 카카오톡에서 가게 정보를 고치고 사이트를 발행할 수 있습니다.'
|
||||
: '연결해 두면 카카오톡 대화창에서 가게 정보를 고치고 사이트를 발행할 수 있습니다.'}
|
||||
{linked
|
||||
? '연결되어 있습니다. 카카오톡에서 가게 정보를 고치고 사이트를 발행할 수 있습니다.'
|
||||
: '연결해 두면 카카오톡 대화창에서 가게 정보를 고치고 사이트를 발행할 수 있습니다.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 flex-wrap gap-2">
|
||||
{!linked && (
|
||||
<Button size="sm" variant="primary" disabled={busy || !ready} onClick={issue}>
|
||||
<Button size="sm" variant="primary" disabled={busy} onClick={issue}>
|
||||
{busy ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
) : (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user