Skip to content
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ab6fd81
refactor: 불필요한 호출 제거
eunseongu Feb 16, 2026
26f676b
feat: 패키지 이동 및 타입 추가
eunseongu Feb 16, 2026
8ad34de
feat: SSE EventListener 구현
eunseongu Feb 16, 2026
009c128
feat: 비동기처리를 위한 config 추가
eunseongu Feb 16, 2026
07ce543
feat: sse 이벤트 발송 추가
eunseongu Feb 16, 2026
2c99398
refactor: 컨벤션 통일 및 서비스 의존 최소화
eunseongu Feb 16, 2026
07948a9
Merge remote-tracking branch 'origin/feature/#574' into feature/#585
eunseongu Feb 16, 2026
7da7f5a
Merge remote-tracking branch 'origin/feature/#574' into feature/#585
eunseongu Feb 16, 2026
6f5f56c
feat: 멤버 참여, 나가기 시 SSE 이벤트 발행 추가
eunseongu Feb 16, 2026
3d70c3c
test: 테스트에 SSE 이벤트를 검증하도록 추가
eunseongu Feb 16, 2026
3674410
feat: SSE에서 멤버 변경 이벤트 응답 기능 추가
eunseongu Feb 16, 2026
e2c4a60
refactor: 불필요한 엔티티 생성 제거
eunseongu Feb 18, 2026
7c924f9
refactor: 새롭게 참여한 회원일 때만 이벤트 발행하도록 변경
eunseongu Feb 18, 2026
bbb4157
test: 코드 변경에 따른 테스트 수정
eunseongu Feb 18, 2026
b4b9857
test: 코드 변경에 따른 테스트 수정
eunseongu Feb 18, 2026
2188d80
Merge remote-tracking branch 'origin/develop' into feature/#585
eunseongu Feb 18, 2026
a2be5c8
test: 코드 변경에 따른 테스트 수정
eunseongu Feb 18, 2026
c008e4b
refactor:트랜잭션 밖에서 lazy 로딩 문제 발생하지 않도록 변경
eunseongu Feb 18, 2026
950828e
refactor: 비동기 처리 방식 및 스레드 수 변경
eunseongu Feb 18, 2026
9dd5924
refactor: 잔여 큐 계산 변경
eunseongu Feb 18, 2026
89692ae
refactor: 불필요한 호출 제거
eunseongu Feb 18, 2026
875c3d7
feat: 폴더 삭제 후 emitter 정리하는 로직 추가
eunseongu Feb 24, 2026
411b83a
refactor: 중복 로직 제거
eunseongu Feb 24, 2026
24c5a17
faet: 공유 폴더에 이미 참여한 멤버인 경우 예외 던지도록 추가
eunseongu Mar 3, 2026
48e2fee
Merge branch 'develop' of https://github.com/woowacourse-teams/2025-T…
seaniiio Mar 3, 2026
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
2 changes: 2 additions & 0 deletions android/app/src/main/java/com/on/turip/TuripApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class TuripApplication : Application() {
FirebaseInstallationsInitializer(userStorageRepository)
.setupFirebaseInstallationId()

fidProvider.init()

FirebaseCrashlytics.getInstance().setUserId(fidProvider.cachedFid)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ private fun TripDetailScreenContent(
)
}

items(items = uiState.places, key = { it.id }) { place ->
items(items = uiState.places, key = { it.timeLine }) { place ->
PlaceItem(
placeModel = place,
onTimeLineClick = onTimeLineClick,
Expand Down
10 changes: 10 additions & 0 deletions android/app/src/main/res/layout/activity_login.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.login.LoginActivity">

</androidx.constraintlayout.widget.ConstraintLayout>
4 changes: 2 additions & 2 deletions android/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[versions]
# App Version
versionName = "1.3.4"
versionCode = "136"
versionName = "1.3.5"
versionCode = "137"

# Gradle Plugin
agp = "8.9.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package turip.account.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import turip.account.domain.Guest;
import turip.account.service.GuestService;
import turip.auth.resolver.AuthGuest;
import turip.common.exception.ErrorResponse;

@RestController
@RequiredArgsConstructor
@RequestMapping("/guests")
@Tag(name = "Guest", description = "게스트 API")
public class GuestController {

private final GuestService guestService;

@Operation(
summary = "게스트 탈퇴 api",
description = "게스트를 삭제한다."
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "204",
description = "성공 예시"
),
@ApiResponse(
responseCode = "400",
description = "실패 예시",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(
name = "guest not found",
summary = "요청 헤더에 device-fid가 존재하지 않는 경우",
value = """
{
"tag": "DEVICE_FID_REQUIRED",
"message": "요청 헤더에 device_fid가 존재하지 않습니다."
}
"""
)
)
)
})
@DeleteMapping("/me")
public ResponseEntity<Void> delete(@Parameter(hidden = true) @AuthGuest Guest guest) {
guestService.delete(guest);
return ResponseEntity.noContent().build();
}
}
144 changes: 144 additions & 0 deletions backend/src/main/java/turip/account/controller/MemberController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package turip.account.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import turip.auth.resolver.AuthGuest;
import turip.auth.resolver.AuthMember;
import turip.common.exception.ErrorResponse;
import turip.account.domain.Guest;
import turip.account.domain.Member;
import turip.account.service.MemberService;

@RestController
@RequiredArgsConstructor
@RequestMapping("/members")
@Tag(name = "Member", description = "회원 API")
public class MemberController {

private final MemberService memberService;

@Operation(
summary = "마이그레이션 api",
description = "게스트의 데이터를 멤버의 데이터로 마이그레이션한다."
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "204",
description = "성공 예시"
),
@ApiResponse(
responseCode = "401",
description = "실패 예시",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(
name = "access token expired",
summary = "만료된 access token",
value = """
{
"tag": "ACCESS_TOKEN_EXPIRED",
"message": "access token이 만료됐습니다."
}
"""
),
@ExampleObject(
name = "invalid signature access token",
summary = "서명값이 올바르지 않은 access token",
value = """
{
"tag": "ACCESS_TOKEN_SIGNATURE_INVALID",
"message": "access token이 위조됐습니다."
}
"""
),
@ExampleObject(
name = "unauthorized",
summary = "알 수 없는 이유로 인증 실패",
value = """
{
"tag": "UNAUTHORIZED",
"message": "토큰 기반 인증에 실패했습니다."
}
"""
)
}
)
)
})
@PostMapping("/migration")
public ResponseEntity<Void> migrate(@Parameter(hidden = true) @AuthMember Member member,
@Parameter(hidden = true) @AuthGuest Guest guest) {
memberService.migrate(member, guest);
return ResponseEntity.noContent().build();
}

@Operation(
summary = "회원 탈퇴 api",
description = "회원을 삭제한다."
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "204",
description = "성공 예시"
),
@ApiResponse(
responseCode = "401",
description = "실패 예시",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(
name = "access token expired",
summary = "만료된 access token",
value = """
{
"tag": "ACCESS_TOKEN_EXPIRED",
"message": "access token이 만료됐습니다."
}
"""
),
@ExampleObject(
name = "invalid signature access token",
summary = "서명값이 올바르지 않은 access token",
value = """
{
"tag": "ACCESS_TOKEN_SIGNATURE_INVALID",
"message": "access token이 위조됐습니다."
}
"""
),
@ExampleObject(
name = "unauthorized",
summary = "알 수 없는 이유로 인증 실패",
value = """
{
"tag": "UNAUTHORIZED",
"message": "토큰 기반 인증에 실패했습니다."
}
"""
)
}
)
)
})
@DeleteMapping("/me")
public ResponseEntity<Void> delete(@Parameter(hidden = true) @AuthMember Member member) {
memberService.delete(member);
return ResponseEntity.noContent().build();
}
}
26 changes: 26 additions & 0 deletions backend/src/main/java/turip/account/domain/Account.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package turip.account.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@AllArgsConstructor
@Table(name = "account")
@NoArgsConstructor(access = AccessLevel.PUBLIC)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

JPA no-args 생성자 접근 레벨을 PROTECTED로 변경하세요

JPA는 protected 이상의 no-args 생성자만 요구합니다. PUBLIC으로 설정하면 new Account()id = null인 인스턴스를 쉽게 생성할 수 있어 의도치 않은 NPE를 유발할 수 있습니다. MemberRefreshToken 엔티티는 모두 AccessLevel.PROTECTED를 사용하여 일관성을 유지하고 있습니다.

🛡️ 제안 수정
-@NoArgsConstructor(access = AccessLevel.PUBLIC)
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@NoArgsConstructor(access = AccessLevel.PUBLIC)
`@NoArgsConstructor`(access = AccessLevel.PROTECTED)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/turip/account/domain/Account.java` at line 18, Change
the no-args constructor access for the Account entity from public to protected:
replace the annotation `@NoArgsConstructor`(access = AccessLevel.PUBLIC) on the
Account class with AccessLevel.PROTECTED so the JPA-required no-args constructor
is present but prevents accidental public instantiation (matching Member and
RefreshToken); ensure the class-level annotation is updated accordingly.

@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Account {

@Id
@EqualsAndHashCode.Include
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
57 changes: 57 additions & 0 deletions backend/src/main/java/turip/account/domain/Member.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package turip.account.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@AllArgsConstructor
@Table(name = "member", uniqueConstraints = {
@UniqueConstraint(name = "uq_member__provider_provider_id", columnNames = {"provider", "provider_id"})
})
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

@Id
@EqualsAndHashCode.Include
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "account_id", nullable = false, unique = true, foreignKey = @ForeignKey(name = "fk_member__account"))
private Account account;

@Enumerated(EnumType.STRING)
@Column(name = "provider", nullable = false)
private Provider provider;

@Column(name = "provider_id", nullable = false)
private String providerId;

@Column(name = "email")
private String email;

public Member(Account account, Provider provider, String providerId, String email) {
this.account = account;
this.provider = provider;
this.providerId = providerId;
this.email = email;
}
}
6 changes: 6 additions & 0 deletions backend/src/main/java/turip/account/domain/Provider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package turip.account.domain;

public enum Provider {

GOOGLE, KAKAO;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package turip.account.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import turip.account.domain.Account;

public interface AccountRepository extends JpaRepository<Account, Long> {


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package turip.account.repository;

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import turip.account.domain.Guest;

public interface GuestRepository extends JpaRepository<Guest, Long> {

Optional<Guest> findByDeviceFid(String deviceFid);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package turip.account.repository;

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import turip.account.domain.Member;
import turip.account.domain.Provider;

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {

boolean existsByProviderAndProviderId(Provider provider, String providerId);

Optional<Member> findByProviderAndProviderId(Provider provider, String providerId);

Optional<Member> findByAccountId(Long accountId);
}
Loading