feat(528): 신규 프로젝트 그룹 공통 백엔드 — 종류 칸 · 산림청 판정 · D01 · E01 생성 API · 공유 GET 만 · 가입 go.kr = 산림청

- DB 020(추가만): projects 종류 · 사업연도 끝 · 설계속도 · 원본 칸 · plan_route_years · plan_route_assignees · 회사 「산림청」 행
- is_forest_email · is_forest_service · /api/auth/session 에 is_forest_service
- require_project_access 한 곳 — 지정 회사 = GET · HEAD 만 · 그 밖 403
- POST /api/d01/project(산림청만 · PLAN · 설계속도 기본 = resolve_design_speed) · GET /api/dashboard/plan-routes/assigned
- GET /api/e01/source-candidates · POST /api/e01/project(B06 완료만 · STABILITY · 원본 칸 · 자료 복사는 528-4)
- 종류별 단계 시드(PLAN 3 · STABILITY 4) · storage 허용 D0x · E0x · B01 목록에 project_kind
- 가입 인증 — 메일 *.go.kr = 회사 산림청 · ACTIVE · 화면 회사 칸 「산림청」 잠금 표시
- 지킴 시험 test_528_project_groups.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 19:32:25 +09:00
co-authored by Claude Opus 5.5
parent bc39953fbd
commit ff8a8bad08
14 changed files with 692 additions and 15 deletions
+23 -6
View File
@@ -148,15 +148,32 @@ async def consume_otp(otp_id: int) -> None:
await connection.commit()
async def complete_registration(user_id: int, is_master: bool) -> None:
async def complete_registration(user_id: int, is_master: bool, forest: bool = False) -> str:
"""메일 인증 끝 — 계정 상태를 돌려줌. forest(*.go.kr) = 회사 「산림청」 고정 · ACTIVE."""
pool = get_db_pool()
async with pool.acquire() as connection, connection.cursor() as cursor:
await cursor.execute(
"""UPDATE users SET status = 'NO_COMPANY', last_email_verified_at = CURRENT_TIMESTAMP
WHERE id = %s""",
(user_id,),
)
company = None
if forest:
await cursor.execute(
"SELECT id FROM companies WHERE name = '산림청' AND deleted_at IS NULL"
)
company = await cursor.fetchone()
if company:
await cursor.execute(
"""UPDATE users SET status = 'ACTIVE', company_id = %s, role = 'USER',
last_email_verified_at = CURRENT_TIMESTAMP
WHERE id = %s""",
(company[0], user_id),
)
else:
await cursor.execute(
"""UPDATE users SET status = 'NO_COMPANY',
last_email_verified_at = CURRENT_TIMESTAMP
WHERE id = %s""",
(user_id,),
)
await connection.commit()
return "ACTIVE" if company else "NO_COMPANY"
async def company_master_email(company_id: int) -> str | None: