Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bce7dbc
feat : ๋‹‰๋„ค์ž„ ์ค‘๋ณต ์ฒดํฌ (nickname unique), ์ธ์ฆ์ฝ”๋“œ ๊ฒ€์‚ฌ ์˜ˆ์™ธ์ฒ˜๋ฆฌ ์ถ”๊ฐ€
yyytir777 Dec 8, 2025
3573613
fix : user_id IDENTITY strategy & dev redis host ์ด๋ฆ„๋ณ€๊ฒฝ (localhost -> rโ€ฆ
yyytir777 Dec 8, 2025
08cfb63
test์ฝ”๋“œ ์ƒ์„ฑ & swagger url ์‚ญ์ œ & ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ค‘๋ณต ์‚ญ์ œ
yyytir777 Dec 8, 2025
ac96fa0
fix : ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€
yyytir777 Dec 9, 2025
43e75ed
Merge pull request #27 from tinybite-2025/feature/13-user
yyytir777 Dec 9, 2025
a70941b
Feature/26 notification (#29)
marshmallowing Dec 11, 2025
cbbf597
feat : google login ๊ตฌํ˜„ ์™„๋ฃŒ
yyytir777 Dec 11, 2025
d03fd30
fix : main push ์‹œ์—๋งŒ workflow trigger
yyytir777 Dec 11, 2025
00cc289
Merge branch 'main' of https://github.com/tinybite-2025/tinybite-servโ€ฆ
yyytir777 Dec 11, 2025
729ae0a
Feature/#28 google apple login
yyytir777 Dec 16, 2025
b5c29db
Merge branch 'main' into develop
yyytir777 Dec 16, 2025
2e5da55
Merge branch 'develop' of https://github.com/tinybite-2025/tinybite-sโ€ฆ
yyytir777 Dec 18, 2025
1fbd896
merge main into develop : main์˜ ํ•ซํ”ฝ์Šค ๋ณ€๊ฒฝ์‚ฌํ•ญ develop์— ๋ฐ˜์˜
yyytir777 Dec 18, 2025
dd9771a
workflow ์ค„๋ฐ”๊ฟˆ ์—๋Ÿฌ ์ˆ˜์ •
yyytir777 Dec 18, 2025
38de611
hotifx : ์—๋Ÿฌ ํ•ธ๋“ค๋ง ์ˆ˜์ • ๋ฐ ๋ฌด์ค‘๋‹จ ๋ฐฐํฌ ์‚ญ์ œ (๋ฆฌ์†Œ์Šค ๋„ˆ๋ฌด ๋งŽ์ด ๋จน์Œ)
yyytir777 Dec 19, 2025
91b8cba
main์˜ ํ•ซํ”ฝ์Šค develop์— ๋ฐ˜์˜
yyytir777 Dec 22, 2025
fbc5e90
์ˆ˜์ •์‚ฌํ•ญ ๋ฐ˜์˜ (API ์ธ์ฆ ๊ด€๋ จ, db schema, ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋“ฑ..)
yyytir777 Dec 23, 2025
7b9fb7b
main๋ธŒ๋žœ์น˜ ํ•ซํ”ฝ์Šค ๋ฐ˜์˜
yyytir777 Dec 24, 2025
bbb080f
Feature/35 term (#38)
yyytir777 Dec 24, 2025
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
6 changes: 3 additions & 3 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ jobs:
export GHCR_USERNAME=${{ github.actor }}
echo $GHCR_TOKEN | docker login ghcr.io -u $GHCR_USERNAME --password-stdin

docker compose -f docker-compose.blue.yml pull
docker compose -f docker-compose.blue.yml down || true
docker compose -f docker-compose.blue.yml up -d
docker-compose -f docker-compose.blue.yml pull
docker-compose -f docker-compose.blue.yml down || true
docker-compose -f docker-compose.blue.yml up -d

docker image prune -a -f
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

import ita.tinybite.domain.user.constant.PlatformType;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;

import java.util.List;

public record GoogleAndAppleSignupRequest(
@NotBlank(message = "idToken์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค")
String idToken,
Expand All @@ -14,6 +17,8 @@ public record GoogleAndAppleSignupRequest(
@NotBlank(message = "์œ„์น˜ ์ •๋ณด ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค")
String location,
@NotNull(message = "ํ”Œ๋žซํผ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค")
PlatformType platform
PlatformType platform,
@NotEmpty
List<String> agreedTerms
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package ita.tinybite.domain.auth.repository;

import ita.tinybite.domain.user.entity.Term;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface TermRepository extends JpaRepository<Term, Long> {

@Query("SELECT t " +
"FROM Term t " +
"WHERE t.title IN :titles")
List<Term> findAllByTitle(List<String> titles);

@Query("SELECT t " +
"FROM Term t " +
"WHERE t.required = true")
List<Term> findRequiredTerm();

}
31 changes: 27 additions & 4 deletions src/main/java/ita/tinybite/domain/auth/service/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@
import ita.tinybite.domain.auth.kakao.KakaoApiClient;
import ita.tinybite.domain.auth.kakao.KakaoApiClient.KakaoUserInfo;
import ita.tinybite.domain.auth.repository.RefreshTokenRepository;
import ita.tinybite.domain.auth.repository.TermRepository;
import ita.tinybite.domain.user.constant.LoginType;
import ita.tinybite.domain.user.constant.PlatformType;
import ita.tinybite.domain.user.constant.UserStatus;
import ita.tinybite.domain.user.entity.Term;
import ita.tinybite.domain.user.entity.User;
import ita.tinybite.domain.user.entity.UserTermAgreement;
import ita.tinybite.domain.user.repository.UserRepository;
import ita.tinybite.global.exception.BusinessException;
import ita.tinybite.global.exception.errorcode.AuthErrorCode;
Expand All @@ -29,15 +32,14 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.io.*;
import java.security.GeneralSecurityException;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

@Slf4j
Expand All @@ -46,11 +48,11 @@
public class AuthService {

private final UserRepository userRepository;
private final TermRepository termRepository;
private final RefreshTokenRepository refreshTokenRepository;
private final KakaoApiClient kakaoApiClient;
private final JwtTokenProvider jwtTokenProvider;
private final JwtDecoder appleJwtDecoder;
private final NicknameGenerator nicknameGenerator;

@Value("${apple.client-id}")
private String appleClientId;
Expand Down Expand Up @@ -147,8 +149,29 @@ public AuthResponse googleSignup(@Valid GoogleAndAppleSignupRequest req) {

// reqํ•„๋“œ๋กœ ์œ ์ € ํ•„๋“œ ์—…๋ฐ์ดํŠธ -> ์‹ค์งˆ์  ํšŒ์›๊ฐ€์ž…
user.updateSignupInfo(req, email, LoginType.GOOGLE);
userRepository.save(user);

// ์š”์ฒญ์—์„œ ๋™์˜ํ•œ termId ์กฐํ•ฉ
List<Term> terms = termRepository.findAllByTitle(req.agreedTerms());

// ์•ฝ๊ด€์˜ ์ด๋ฆ„์ด ์ผ์น˜ํ•˜์ง€ ์•Š์•„ ์‚ฌ์ด์ฆˆ๊ฐ€ ๋‹ค๋ฅผ ๋•Œ -> ์ž˜๋ชป๋œ ์•ฝ๊ด€ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
if(!(terms.size() == req.agreedTerms().size())) {
throw BusinessException.of(AuthErrorCode.INVALID_TERM);
}

// ํ•„์ˆ˜๋กœ ๋™์˜ํ•ด์•ผํ•˜๋Š” ํ•ญ๋ชฉ์— ๋ชจ๋‘ ๋™์˜๋ฅผ ํ•˜์ง€ ์•Š์•˜์„ ๋•Œ -> ํ•„์ˆ˜ ์•ฝ๊ด€์— ๋™์˜ํ•˜์„ธ์š”
if(!terms.containsAll(termRepository.findRequiredTerm())) {
throw BusinessException.of(AuthErrorCode.PLEASE_AGREE_TERM);
}

// ์œ ์ € - ์•ฝ๊ด€ ๊ฐ„ ๋™์˜ ํ•ญ๋ชฉ ์ƒ์„ฑ
List<UserTermAgreement> agreements = terms
.stream().map(term -> UserTermAgreement.builder()
.user(user)
.term(term)
.build()).toList();

user.addTerms(agreements);
userRepository.save(user);
return getAuthResponse(user);
}

Expand Down
29 changes: 29 additions & 0 deletions src/main/java/ita/tinybite/domain/user/entity/Term.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package ita.tinybite.domain.user.entity;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "terms")
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Term {

@Id
@Column(name = "term_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true)
private String title;

private String description;

private boolean required;

private int version;

// term์—๋Š” user_term_agreement ํ…Œ์ด๋ธ” ์—ฐ๊ด€๊ด€๊ณ„ ํ•„์š” X
}
10 changes: 10 additions & 0 deletions src/main/java/ita/tinybite/domain/user/entity/TermCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ita.tinybite.domain.user.entity;

public enum TermCode {
AGE_OVER_14, // (ํ•„์ˆ˜) ๋งŒ 14์„ธ ์ด์ƒ
SERVICE_USE, // (ํ•„์ˆ˜) ์„œ๋น„์Šค ์ด์šฉ์•ฝ๊ด€ ๋™์˜
ELECTRONIC_FINANCE, // (ํ•„์ˆ˜) ์ „์ž๊ธˆ์œต๊ฑฐ๋ž˜ ์ด์šฉ์•ฝ๊ด€ ๋™์˜
PRIVACY_COLLECT, // (ํ•„์ˆ˜) ๊ฐœ์ธ์ •๋ณด ์ˆ˜์ง‘ ์ด์šฉ ๋™์˜
PRIVACY_PROVIDE, // (ํ•„์ˆ˜) ๊ฐœ์ธ์ •๋ณด ์ œ๊ณต ๋™์˜
MARKETING_RECEIVE // (์„ ํƒ) ์‡ผํ•‘์ •๋ณด ๋ฐ ํ˜œํƒ ์ˆ˜์‹  ๋™์˜
}
10 changes: 10 additions & 0 deletions src/main/java/ita/tinybite/domain/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import lombok.*;
import org.hibernate.annotations.Comment;

import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "users")
@Getter
Expand Down Expand Up @@ -43,6 +46,9 @@ public class User extends BaseEntity {
@Column(length = 100)
private String location;

@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<UserTermAgreement> agreements = new ArrayList<>();;

public void update(UpdateUserReqDto req) {
this.nickname = req.nickname();
}
Expand All @@ -59,4 +65,8 @@ public void updateSignupInfo(GoogleAndAppleSignupRequest req, String email, Logi
this.status = UserStatus.ACTIVE;
this.type = loginType;
}

public void addTerms(List<UserTermAgreement> agreements) {
this.agreements.addAll(agreements);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package ita.tinybite.domain.user.entity;

import ita.tinybite.global.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.*;


@Entity
@Table(name = "user_term_agreements",
uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "term_id"})})
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserTermAgreement extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_term_agreement_id")
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "term_id", nullable = false)
private Term term;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ public enum AuthErrorCode implements ErrorCode {

INVALID_PLATFORM(HttpStatus.BAD_REQUEST, "INVALID_PLATFORM", "์˜ฌ๋ฐ”๋ฅธ ํ”Œ๋žซํผ์ด ์•„๋‹™๋‹ˆ๋‹ค. (Android, iOS)"),
NOT_EXISTS_EMAIL(HttpStatus.BAD_REQUEST, "NOT_EXISTS_EMAIL", "์• ํ”Œ ์ด๋ฉ”์ผ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."),
INVALID_LOCATION(HttpStatus.BAD_REQUEST, "INVALID_LOCATION", "์œ„์น˜ ์ •๋ณด๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
INVALID_LOCATION(HttpStatus.BAD_REQUEST, "INVALID_LOCATION", "์œ„์น˜ ์ •๋ณด๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."),
INVALID_TERM(HttpStatus.BAD_REQUEST, "INVALID_TERM", "์ž˜๋ชป๋œ ์•ฝ๊ด€์ž…๋‹ˆ๋‹ค."),

PLEASE_AGREE_TERM(HttpStatus.BAD_REQUEST, "PLEASE_AGREE_TERM", "ํ•„์ˆ˜ ํ•ญ๋ชฉ์— ๋™์˜ํ•˜์ง€ ์•Š์œผ์…จ์Šต๋‹ˆ๋‹ค.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/ita/tinybite/global/health/LoginReqDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package ita.tinybite.global.health;

public record LoginReqDto(String email) {
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
package ita.tinybite.global.health.controller;

import io.swagger.v3.oas.annotations.Operation;
import ita.tinybite.global.exception.BusinessException;
import ita.tinybite.global.exception.errorcode.BusinessErrorCode;
import ita.tinybite.global.health.LoginReqDto;
import ita.tinybite.global.health.service.AuthTestService;
import ita.tinybite.global.response.APIResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import static ita.tinybite.global.response.APIResponse.*;

@RestController
public class HealthCheckController {

private final AuthTestService authTestService;

public HealthCheckController(AuthTestService authTestService) {
this.authTestService = authTestService;
}

@GetMapping("/test/health")
public ResponseEntity<String> health() {
return ResponseEntity.ok("UP");
}

@GetMapping("/api/v1/test/health")
public APIResponse<?> test() {
return APIResponse.success("test");
return success("test");
}

@GetMapping("/api/v1/test/error/business")
Expand All @@ -30,4 +39,10 @@ public APIResponse<?> businessError() {
public APIResponse<?> commonError() throws Exception {
throw new Exception("INTERNAL_SERVER_ERROR");
}

@PostMapping("/api/v1/test/login")
@Operation(summary = "๋ฐฑ์—”๋“œ์—์„œ ์œ ์ € ์ธ์ฆ์„ ์œ„ํ•œ API")
public APIResponse<?> login(@RequestBody LoginReqDto req) {
return success(authTestService.getUser(req.email()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package ita.tinybite.global.health.service;

import ita.tinybite.domain.auth.entity.JwtTokenProvider;
import ita.tinybite.domain.user.constant.LoginType;
import ita.tinybite.domain.user.constant.UserStatus;
import ita.tinybite.domain.user.entity.User;
import ita.tinybite.domain.user.repository.UserRepository;
import org.springframework.stereotype.Service;

@Service
public class AuthTestService {

private final UserRepository userRepository;
private final JwtTokenProvider jwtTokenProvider;

public AuthTestService(UserRepository userRepository, JwtTokenProvider jwtTokenProvider) {
this.userRepository = userRepository;
this.jwtTokenProvider = jwtTokenProvider;
}

public String getUser(String email) {
User user = userRepository.findByEmail(email).orElseGet(() ->
{
User newUser = User.builder()
.email(email)
.type(LoginType.GOOGLE)
.status(UserStatus.ACTIVE)
.nickname(email)
.build();
userRepository.save(newUser);
return newUser;
});

return jwtTokenProvider.generateAccessToken(user);
}
}
3 changes: 1 addition & 2 deletions src/main/resources/application-local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ spring:
jpa:
open-in-view: false
hibernate:
ddl-auto: create-drop
ddl-auto: update
properties:
hibernate:
format_sql: true
show_sql: true
dialect: org.hibernate.dialect.MySQLDialect

data:
Expand Down
23 changes: 23 additions & 0 deletions src/main/resources/term/terms.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- ๋งŒ 14์„ธ ์ด์ƒ
INSERT INTO terms (title, description, required, version)
VALUES ('AGE_OVER_14', '๋ณธ ์„œ๋น„์Šค๋Š” ๋งŒ 14์„ธ ์ด์ƒ๋งŒ ์ด์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.', true, 1);

-- ์„œ๋น„์Šค ์ด์šฉ์•ฝ๊ด€
INSERT INTO terms (title, description, required, version)
VALUES ('SERVICE_USE', '์„œ๋น„์Šค ์ด์šฉ์„ ์œ„ํ•œ ๊ธฐ๋ณธ ์•ฝ๊ด€์ž…๋‹ˆ๋‹ค.', true, 1);

-- ์ „์ž๊ธˆ์œต๊ฑฐ๋ž˜ ์ด์šฉ์•ฝ๊ด€
INSERT INTO terms (title, description, required, version)
VALUES ('ELECTRONIC_FINANCE', '์ „์ž๊ธˆ์œต๊ฑฐ๋ž˜ ๊ด€๋ จ ์•ฝ๊ด€์ž…๋‹ˆ๋‹ค.', true, 1);

-- ๊ฐœ์ธ์ •๋ณด ์ˆ˜์ง‘ ์ด์šฉ
INSERT INTO terms (title, description, required, version)
VALUES ('PRIVACY_COLLECT', '์„œ๋น„์Šค ์ œ๊ณต์„ ์œ„ํ•œ ๊ฐœ์ธ์ •๋ณด ์ˆ˜์ง‘ ๋ฐ ์ด์šฉ์— ๋Œ€ํ•œ ๋™์˜์ž…๋‹ˆ๋‹ค.', true, 1);

-- ๊ฐœ์ธ์ •๋ณด ์ œ๊ณต
INSERT INTO terms (title, description, required, version)
VALUES ('PRIVACY_PROVIDE', '์ œ3์ž์—๊ฒŒ ๊ฐœ์ธ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์— ๋Œ€ํ•œ ๋™์˜์ž…๋‹ˆ๋‹ค.', true, 1);

-- ๋งˆ์ผ€ํŒ… ์ •๋ณด ์ˆ˜์‹ 
INSERT INTO terms (title, description, required, version)
VALUES ('MARKETING_RECEIVE', '์ด๋ฒคํŠธ ๋ฐ ํ˜œํƒ ์ •๋ณด ์ˆ˜์‹ ์— ๋Œ€ํ•œ ๋™์˜์ž…๋‹ˆ๋‹ค.', false, 1);