Files
Aislo/B01_Dashboard/B01_Dashboard_Schema.py
T
eomsangdonandClaude Opus 5 8a0a8f18e3 feat(화면): 화면 배치를 계정에 저장 — PC 를 바꿔도 따라오게
사용자 승인(2026-09-07). 앞서 취향을 localStorage 로 옮겨 **탭·재시작** 문제는 풀었고,
이 변경은 그 위에 **다른 PC 에서도 같은 배치**를 얹음. 사용자가 노트북·데스크톱 두 대를 오감.

- `db_management/018_user_ui_prefs.sql` — 사용자당 한 줄, `prefs` JSON 한 칸.
  칸을 나누면 취향이 늘 때마다 마이그레이션이 또 필요해 한 칸에 담음.
- `GET/PUT /api/dashboard/me/ui-prefs` — 배치 값만. **설계값은 안 담음**(문자열만 받음).
- 로그인이 확인된 첫 순간에 한 번 받아 로컬 위에 얹고, 취향이 바뀌면 1.5초 모아 올림.
  서버가 없거나 못 읽으면 **로컬 값으로 그대로 돔**(계획서 원문).

⚠ 같은 함정을 또 밟을 뻔했음 — 키만 아는 자리가 저장소를 직접 고르면 **올려보내기도 안 걸림**.
그래서 `storageOf` 를 없애고 **읽기·쓰기 창구 하나**(`readByKey`/`writeByKey`)로 모음.
저장소 선택과 서버 올려보내기가 그 한 곳에만 있음. 그물도 그 이름으로 갱신.

**DB 는 아직 적용하지 않았음** — 표가 없으면 API 가 실패하고 화면은 로컬 값으로 도는 것이
정상 동작임. 적용 시점은 다른 창들과 맞춘 뒤.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 14:50:31 +09:00

106 lines
4.5 KiB
Python

"""B01_Dashboard 요청 스키마."""
from datetime import date
from pydantic import BaseModel, Field
class UpdateUserRequest(BaseModel):
name: str = Field(min_length=1, max_length=100)
position: str | None = Field(default=None, max_length=100)
department: str | None = Field(default=None, max_length=100)
phone: str | None = Field(default=None, max_length=50)
class UiPrefsRequest(BaseModel):
"""화면 취향 묶음 — 배치·표시 값만. 키는 브라우저 등록표(`b_page_state.ts`)가 정본이다.
값은 문자열 하나(높이는 `"438"`, 접힘은 `"true"`)라 저장소에 넣던 모양 그대로다.
설계값이 섞여 들어오지 않게 **문자열만** 받는다.
"""
prefs: dict[str, str] = Field(default_factory=dict)
class CreateCompanyRequest(BaseModel):
name: str = Field(min_length=1, max_length=255)
business_registration_number: str = Field(min_length=1, max_length=20)
business_address: str | None = Field(default=None, max_length=500)
business_owner: str | None = Field(default=None, max_length=100)
class JoinCompanyRequest(BaseModel):
company_id: int = Field(gt=0)
class AddMemberRequest(BaseModel):
# 팀원 등록은 「이미 가입한 사람을 고르는」 일이다 (2026-09-06 사용자 확정) —
# 계정을 대신 만들지 않는다. 소속이 없는 가입자만 고를 수 있다.
user_id: int = Field(gt=0)
class InviteMemberRequest(BaseModel):
"""아직 가입하지 않은 사람에게 보내는 가입 안내 메일."""
email: str = Field(min_length=3, max_length=255)
name: str | None = Field(default=None, max_length=100)
class UpdateCompanyLogoRequest(BaseModel):
# 회사 대표 로고 (company_assets.id, kind=LOGO). None 이면 지정을 지운다.
logo_asset_id: int | None = Field(default=None, gt=0)
class ProcessJoinRequest(BaseModel):
action: str = Field(pattern="^(APPROVE|REJECT)$")
reason: str | None = Field(default=None, max_length=500)
class ChangeUserRoleRequest(BaseModel):
role: str = Field(pattern="^(SYSTEM_ADMIN|ADMIN|USER)$")
class AssignCompanyRequest(BaseModel):
company_id: int | None = Field(default=None, gt=0)
class UpdateProjectRequest(BaseModel):
name: str = Field(min_length=1, max_length=255)
# 참여자 (2026-09-06 사용자 확정) — 도면 표제란 3역할과 별개로, 설계에 손대는 사람들.
# 참여자면 일반 사용자도 그 프로젝트를 수정할 수 있다. 비우면 지금 값을 그대로 둔다.
member_user_ids: list[int] | None = Field(default=None)
region: str | None = Field(default=None, max_length=100)
road_type: str | None = Field(default=None, max_length=100)
project_year: int | None = Field(default=None, ge=1900, le=2100)
estimated_length_m: float | None = Field(default=None, ge=0)
# 계획노선 사용 범위 (2026-09-04 사용자 지시) — 비우면 전 구간.
route_start_m: float | None = Field(default=None, ge=0)
route_end_m: float | None = Field(default=None, ge=0)
memo: str | None = Field(default=None, max_length=5000)
status: str | None = Field(default=None, max_length=50)
# 도면 표제란·표지에 실리는 값 (2026-09-02). 프로그램이 지어낼 수 없어 사람이 넣는다.
client_org: str | None = Field(default=None, max_length=255)
project_number: str | None = Field(default=None, max_length=100)
work_amount: str | None = Field(default=None, max_length=100)
# 설계일자는 확정일 자동이 아니라 사용자 지정이다 (2026-09-02 사용자 확정).
design_date: date | None = None
# 담당자는 회사 구성원(users) 중에서, 로고·서명은 회사 공유 자산(company_assets) 중에서
# 고른다 — 같은 회사 것인지는 라우터가 확인한다.
pm_user_id: int | None = Field(default=None, gt=0)
field_lead_user_id: int | None = Field(default=None, gt=0)
designer_user_id: int | None = Field(default=None, gt=0)
logo_asset_id: int | None = Field(default=None, gt=0)
signature_asset_id: int | None = Field(default=None, gt=0)
class AdminUpdateUserRequest(UpdateUserRequest):
status: str | None = Field(
default=None, pattern="^(NO_COMPANY|PENDING|ACTIVE|INACTIVE|REJECTED)$"
)
class UpdateCompanyAssetRequest(BaseModel):
label: str = Field(min_length=1, max_length=100)
# 사용자 계정에 물릴 수도(개인 서명) 아닐 수도(회사 공용 직인) 있다.
user_id: int | None = Field(default=None, gt=0)