Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@

CORS_ORIGINS = [
"http://localhost:3000",
"http://127.0.0.1:3000",
"http://localhost:5173",
"http://127.0.0.1:5173",
]
54 changes: 52 additions & 2 deletions backend/app/models/schemas.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from enum import Enum

from pydantic import BaseModel, field_validator
from pydantic import BaseModel, Field, field_validator, model_validator


class SortOrder(str, Enum):
Expand All @@ -13,9 +13,11 @@ class RoomCreate(BaseModel):
options: list[str]
password: str | None = None
ttl: int = 3600
tags: list[str] = []
tags: list[str] = Field(default_factory=list)
allow_multiple: bool = False
is_private: bool = False
participants: list[str] = Field(default_factory=list)
option_allowed_participants: list[list[str]] | None = None

@field_validator('options')
@classmethod
Expand All @@ -24,6 +26,19 @@ def validate_options(cls, v):
raise ValueError('최소 2개의 옵션이 필요합니다')
return v

@field_validator('participants')
@classmethod
def validate_participants(cls, v):
participants = []
seen = set()
for participant in v:
name = participant.strip()
if not name or name in seen:
continue
participants.append(name)
seen.add(name)
return participants

@field_validator('tags')
@classmethod
def validate_tags(cls, v):
Expand All @@ -34,10 +49,40 @@ def validate_tags(cls, v):
raise ValueError('태그는 20자 이내여야 합니다')
return v

@model_validator(mode='after')
def validate_option_allowed_participants(self):
if self.option_allowed_participants is None:
return self

if not self.participants:
raise ValueError('참여 가능 인원을 설정하려면 참여 인원이 필요합니다')

if len(self.option_allowed_participants) != len(self.options):
raise ValueError('선택지별 참여 가능 인원 배열은 선택지 개수와 같아야 합니다')

participant_names = set(self.participants)
normalized_permissions = []
for allowed_participants in self.option_allowed_participants:
option_permissions = []
seen = set()
for participant in allowed_participants:
name = participant.strip()
if not name or name in seen:
continue
if name not in participant_names:
raise ValueError(f'참여 인원에 없는 이름입니다: {name}')
option_permissions.append(name)
seen.add(name)
normalized_permissions.append(option_permissions)

self.option_allowed_participants = normalized_permissions
return self


class VoteRequest(BaseModel):
options: list[str]
fingerprint: str
participant: str | None = None

@field_validator('options')
@classmethod
Expand All @@ -46,6 +91,11 @@ def validate_options(cls, v):
raise ValueError('최소 1개의 옵션을 선택해야 합니다')
return v

@field_validator('participant')
@classmethod
def validate_participant(cls, v):
return v.strip() if v else None


class PasswordVerifyRequest(BaseModel):
password: str | None = None
Expand Down
26 changes: 25 additions & 1 deletion backend/app/routers/rooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ async def create_room_endpoint(room: RoomCreate):
ttl=room.ttl,
tags=room.tags,
allow_multiple=room.allow_multiple,
is_private=room.is_private
is_private=room.is_private,
participants=room.participants,
option_allowed_participants=room.option_allowed_participants,
)


Expand Down Expand Up @@ -97,6 +99,28 @@ async def vote(room_uuid: str, vote_request: VoteRequest, request: Request):
if not room.get("allow_multiple", False) and len(vote_request.options) > 1:
raise HTTPException(status_code=400, detail="이 투표는 복수 선택이 허용되지 않습니다")

participants = room.get("participants", [])
if participants:
if not vote_request.participant:
raise HTTPException(status_code=400, detail="참여자를 선택해주세요")

if vote_request.participant not in participants:
raise HTTPException(status_code=400, detail="참여 인원에 없는 이름입니다")

option_allowed_participants = room.get("option_allowed_participants", [])
for option in vote_request.options:
option_index = room["options"].index(option)
if option_index < len(option_allowed_participants):
allowed_participants = option_allowed_participants[option_index]
else:
allowed_participants = participants

if vote_request.participant not in allowed_participants:
raise HTTPException(
status_code=403,
detail=f"{vote_request.participant}님은 선택할 수 없는 옵션입니다: {option}",
)

client_ip = request.client.host
if await has_voted(room_uuid, vote_request.fingerprint, client_ip):
raise HTTPException(status_code=409, detail="이미 투표하셨습니다")
Expand Down
10 changes: 9 additions & 1 deletion backend/app/services/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ async def create_room(
ttl: int,
tags: list[str] | None = None,
allow_multiple: bool = False,
is_private: bool = False
is_private: bool = False,
participants: list[str] | None = None,
option_allowed_participants: list[list[str]] | None = None,
) -> dict:
"""투표방 생성"""
redis = get_redis()
Expand All @@ -26,11 +28,17 @@ async def create_room(
timestamp = created_at.timestamp()

tags = tags or []
participants = participants or []
if participants and option_allowed_participants is None:
option_allowed_participants = [participants.copy() for _ in options]
option_allowed_participants = option_allowed_participants or []

room_data = {
"uuid": room_uuid,
"title": title,
"options": options,
"participants": participants,
"option_allowed_participants": option_allowed_participants,
"created_at": created_at.isoformat(),
"expires_at": expires_at.isoformat(),
"has_password": password is not None,
Expand Down
16 changes: 16 additions & 0 deletions backend/tests/test_health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import sys
from pathlib import Path

from fastapi.testclient import TestClient

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from app.main import app


def test_health_check():
with TestClient(app) as client:
response = client.get("/api/health")

assert response.status_code == 200
assert response.json() == {"status": "ok"}
1 change: 0 additions & 1 deletion frontend/app/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ export default function CreatePage() {
localStorage.setItem(key, JSON.stringify(capped));
} catch (storageErr) {
// ignore localStorage errors
// eslint-disable-next-line no-console
console.warn("Failed to save my poll to localStorage", storageErr);
}

Expand Down
Loading
Loading