260706_1_계정, 로그인, 보안 초안 작성

This commit is contained in:
2026-07-07 19:17:13 +09:00
parent 404941c47a
commit f8633bb1fe
4696 changed files with 6110 additions and 642171 deletions
+68
View File
@@ -0,0 +1,68 @@
"""기술지원 문의 요청 API."""
from fastapi import APIRouter, Request
from common_util.common_util_auth import get_optional_session
from common_util.common_util_email import send_email_background
from common_util.common_util_email_templates import support_request_email
from config.config_db import get_db_pool
from config.config_system import ADMIN_EMAIL
from .A08_Support_Schema import SupportRequestPayload
router = APIRouter(prefix="/api/a08", tags=["Support"])
async def _get_company_name(company_id: int | None) -> str | None:
"""회사 ID로 회사명을 조회한다. 없으면 None."""
if not company_id:
return None
pool = get_db_pool()
async with pool.acquire() as connection, connection.cursor() as cursor:
await cursor.execute("SELECT name FROM companies WHERE id = %s", (company_id,))
row = await cursor.fetchone()
return row[0] if row else None
@router.post("/support")
async def submit_support_request(payload: SupportRequestPayload, request: Request):
"""기술지원 문의 요청 접수 및 메일 발송.
로그인 상태면 세션의 회사 정보를 연결하고, 비로그인이면 고객이 입력한 값만 저장한다.
"""
# 로그인 상태면 회사 정보 연결 (비로그인이면 None)
session = await get_optional_session(request)
company_id = session["company_id"] if session else None
company_name = await _get_company_name(company_id)
pool = get_db_pool()
async with pool.acquire() as connection, connection.cursor() as cursor:
await cursor.execute(
"""INSERT INTO support_requests
(company_id, company_name, user_name, user_email, user_phone, subject, message)
VALUES (%s, %s, %s, %s, %s, %s, %s)""",
(
company_id,
company_name,
payload.name,
payload.email,
payload.phone,
payload.subject,
payload.message,
),
)
await connection.commit()
# 관리자에게 문의 알림 메일 발송
if ADMIN_EMAIL:
subject, html = support_request_email(
payload.name,
payload.email,
payload.phone,
company_name,
payload.subject,
payload.message,
)
send_email_background(ADMIN_EMAIL, subject, html)
return {"status": "success", "message": "문의가 접수되었습니다. 곧 연락드리겠습니다."}
+13
View File
@@ -0,0 +1,13 @@
"""기술지원 문의 요청 데이터 모델."""
from pydantic import BaseModel, EmailStr, Field
class SupportRequestPayload(BaseModel):
"""기술지원 문의 요청"""
name: str = Field(..., min_length=1, max_length=255, description="작성자 이름")
email: EmailStr = Field(..., description="작성자 이메일")
phone: str | None = Field(None, max_length=30, description="작성자 연락처")
subject: str = Field(..., min_length=1, max_length=255, description="문의 제목")
message: str = Field(..., min_length=1, max_length=5000, description="문의 내용")
+45 -5
View File
@@ -13,6 +13,7 @@
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
import { createButton, createInputField } from "@ui/ui_template_elements";
import { isBlank, isValidEmail } from "@util/common_util_validate";
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
import "./A08_Support_UI_Style.css";
/** locale 헬퍼 */
@@ -93,12 +94,19 @@ export function renderA08Support(root: HTMLElement): void {
type: "text",
required: true,
});
const emailField = createInputField({
label: L("A08_Support_Field_Email"),
placeholder: L("A08_Support_Field_Email_Placeholder"),
type: "email",
required: true,
});
const phoneField = createInputField({
label: L("A08_Support_Field_Phone"),
placeholder: L("A08_Support_Field_Phone_Placeholder"),
type: "tel",
required: false,
});
const subjectField = createInputField({
label: L("A08_Support_Field_Subject"),
placeholder: L("A08_Support_Field_Subject_Placeholder"),
@@ -132,6 +140,7 @@ export function renderA08Support(root: HTMLElement): void {
const name = nameField.input.value;
const email = emailField.input.value;
const phone = phoneField.input.value;
const subject = subjectField.input.value;
const message = messageField.textarea.value;
@@ -161,16 +170,47 @@ export function renderA08Support(root: HTMLElement): void {
return;
}
// TODO: POST /api/a08/support 연동. 성공 시 폼 초기화 후 성공 메시지 표시.
form.reset();
messageField.textarea.value = "";
successMsg.hidden = false;
// API 호출: POST /api/a08/support
(async () => {
try {
const response = await fetch("/api/a08/support", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, email, phone: phone || null, subject, message }),
});
if (!response.ok) {
const error = (await response.json()) as { detail?: string };
throw new Error(error.detail ?? "요청 실패");
}
form.reset();
messageField.textarea.value = "";
successMsg.hidden = false;
} catch (err) {
const msg = err instanceof Error ? err.message : "오류 발생";
nameField.setError(msg);
}
})();
}
form.addEventListener("submit", onA08_Support_Submit_Click);
form.append(nameField.root, emailField.root, subjectField.root, messageField.root, submitBtn);
form.append(
nameField.root,
emailField.root,
phoneField.root,
subjectField.root,
messageField.root,
submitBtn,
);
card.append(title, subtitle, successMsg, form);
page.append(card);
root.append(page);
// 백그라운드에서 세션 정보 로드 (페이지 렌더링을 지연시키지 않음)
(async () => {
const sessionUser = await fetchSessionUser();
if (sessionUser?.name) nameField.input.value = sessionUser.name;
if (sessionUser?.email) emailField.input.value = sessionUser.email;
})();
}