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
24 changes: 23 additions & 1 deletion src/main/kotlin/com/tripsync/application/room/RoomService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,21 @@ class RoomService(
) {

@Transactional
fun createRoom(host: User, destination: String, tripDate: LocalDate): ApiResponse<Map<String, Any?>> {
fun createRoom(host: User, destination: String, tripDate: LocalDate, roomName: String? = null): ApiResponse<Map<String, Any?>> {
if (host.isGuest) {
throw DomainException(HttpStatus.FORBIDDEN, "FORBIDDEN", "방장 권한이 필요합니다.")
}
if (!tripDate.isAfter(LocalDate.now())) {
throw DomainException(HttpStatus.UNPROCESSABLE_ENTITY, "INVALID_REQUEST", "tripDate는 오늘 이후여야 합니다.")
}

val normalizedRoomName = normalizeRoomName(roomName, destination)
val room = tripRoomRepository.save(
TripRoom(
hostUser = host,
shareCode = generateShareCode(),
destination = destination,
roomName = normalizedRoomName,
tripDate = tripDate,
status = TripRoomStatus.WAITING,
)
Expand All @@ -60,6 +62,7 @@ class RoomService(
return ApiResponse.ok(
mapOf(
"roomId" to room.id,
"roomName" to room.roomName,
"shareCode" to room.shareCode,
"status" to room.status.name.lowercase(),
)
Expand Down Expand Up @@ -230,6 +233,7 @@ class RoomService(
val latestVersion = schedules.maxOfOrNull { it.version }
val base = mapOf(
"roomId" to room.id,
"roomName" to room.roomName,
"destination" to room.destination,
"tripDate" to room.tripDate.toString(),
"tripStartDate" to room.tripDate.toString(),
Expand Down Expand Up @@ -269,8 +273,26 @@ class RoomService(
return base + mapOf("scheduleState" to scheduleState)
}

private fun normalizeRoomName(roomName: String?, destination: String): String {
val normalized = roomName?.trim()?.takeIf { it.isNotBlank() } ?: defaultRoomName(destination)
if (normalized.length > ROOM_NAME_MAX_LENGTH) {
throw DomainException(HttpStatus.UNPROCESSABLE_ENTITY, "INVALID_REQUEST", "방 이름은 100자 이하여야 합니다.")
}
return normalized
}

private fun defaultRoomName(destination: String): String {
val destinationLimit = ROOM_NAME_MAX_LENGTH - ROOM_NAME_SUFFIX.length
return destination.trim().take(destinationLimit) + ROOM_NAME_SUFFIX
}

private fun generateShareCode(): String {
val suffix = UUID.randomUUID().toString().replace("-", "").take(5).uppercase()
return "CNAM${LocalDate.now().year.toString().takeLast(2)}$suffix"
}

private companion object {
const val ROOM_NAME_MAX_LENGTH = 100
const val ROOM_NAME_SUFFIX = " 여행 계획"
}
}
3 changes: 3 additions & 0 deletions src/main/kotlin/com/tripsync/domain/entity/TripRoom.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ class TripRoom(
@Column(nullable = false, length = 100)
var destination: String,

@Column(name = "room_name", nullable = false, length = 100)
var roomName: String,

@Column(name = "trip_date", nullable = false)
var tripDate: LocalDate,

Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/com/tripsync/web/dto/AuthDto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ data class CreateRoomDto(
val tripDate: String,
val tripStartDate: String? = null,
val tripEndDate: String? = null,
val roomName: String? = null,
)

data class JoinRoomDto(
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/com/tripsync/web/room/RoomController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class RoomController(
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun createRoom(@Valid @RequestBody dto: CreateRoomDto, @CurrentUser user: User): ApiResponse<Map<String, Any?>> {
return roomService.createRoom(user, dto.destination, LocalDate.parse(dto.tripDate))
return roomService.createRoom(user, dto.destination, LocalDate.parse(dto.tripDate), dto.roomName)
}

@GetMapping("/my")
Expand Down
7 changes: 7 additions & 0 deletions src/main/resources/db/migration/V7__add_trip_room_name.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ALTER TABLE trip_rooms ADD COLUMN room_name VARCHAR(100);

UPDATE trip_rooms
SET room_name = LEFT(TRIM(destination), 100 - CHAR_LENGTH(' 여행 계획')) || ' 여행 계획'
WHERE room_name IS NULL;

ALTER TABLE trip_rooms ALTER COLUMN room_name SET NOT NULL;
37 changes: 36 additions & 1 deletion src/test/kotlin/com/tripsync/AuthContractTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,44 @@ class AuthContractTests(
jsonPath("$.data.rooms[0].roomId") { value(secondRoomId.toInt()) }
jsonPath("$.data.rooms[1].roomId") { value(firstRoomId.toInt()) }
jsonPath("$.data.rooms[0].destination") { value("충청남도") }
jsonPath("$.data.rooms[0].roomName") { value("충남 봄 여행") }
jsonPath("$.data.rooms[0].memberCount") { value(1) }
}
}


@Test
fun `room name fallback preserves suffix within database limit`() {
val hostSession = registerSession("host-room-name-fallback@example.com", "방이름-fallback")
val destination = "가".repeat(100)
val expectedRoomName = "가".repeat(94) + " 여행 계획"

mockMvc.post("/rooms") {
cookie(hostSession)
contentType = MediaType.APPLICATION_JSON
content = """{"destination":"$destination","tripDate":"${LocalDate.now().plusDays(7)}"}"""
}.andExpect {
status { isCreated() }
jsonPath("$.data.roomName") { value(expectedRoomName) }
}
}

@Test
fun `explicit room name over database limit is rejected`() {
val hostSession = registerSession("host-room-name-too-long@example.com", "방이름-long")
val roomName = "나".repeat(101)

mockMvc.post("/rooms") {
cookie(hostSession)
contentType = MediaType.APPLICATION_JSON
content = """{"destination":"충청남도","tripDate":"${LocalDate.now().plusDays(7)}","roomName":"$roomName"}"""
}.andExpect {
status { isUnprocessableEntity() }
jsonPath("$.success") { value(false) }
jsonPath("$.error.code") { value("INVALID_REQUEST") }
}
}

@Test
fun `oauth start sets state cookie and local callback creates session`() {
val start = mockMvc.get("/auth/google") {
Expand Down Expand Up @@ -229,10 +263,11 @@ class AuthContractTests(
val response = mockMvc.post("/rooms") {
cookie(session)
contentType = MediaType.APPLICATION_JSON
content = """{"destination":"충청남도","tripDate":"${LocalDate.now().plusDays(7)}"}"""
content = """{"destination":"충청남도","tripDate":"${LocalDate.now().plusDays(7)}","roomName":"충남 봄 여행"}"""
}.andExpect {
status { isCreated() }
jsonPath("$.data.roomId") { value(notNullValue()) }
jsonPath("$.data.roomName") { value("충남 봄 여행") }
}.andReturn().response.contentAsString

return Regex("""\"roomId\":(\d+)""").find(response)!!.groupValues[1].toLong()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class ScheduleResponseMapperTest {
hostUser = host,
shareCode = "ABC123456789",
destination = "충남",
roomName = "충남 여행 계획",
tripDate = LocalDate.parse("2026-06-01"),
status = TripRoomStatus.COMPLETED,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ class ScheduleServiceTest(
hostUser = host,
shareCode = "S${suffix.toString().takeLast(10)}",
destination = "충남",
roomName = "충남 여행 계획",
tripDate = LocalDate.now().plusDays(7),
status = TripRoomStatus.COMPLETED,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ class TripPhotoRepositoryTest(
hostUser = host,
shareCode = "P${suffix.toString().takeLast(10)}",
destination = "충남",
roomName = "충남 여행 계획",
tripDate = LocalDate.now().minusDays(1),
status = TripRoomStatus.COMPLETED,
)
Expand Down
Loading