diff --git a/.github/workflows/automatic-erd.yml b/.github/workflows/automatic-erd.yml new file mode 100644 index 00000000..309bd73c --- /dev/null +++ b/.github/workflows/automatic-erd.yml @@ -0,0 +1,96 @@ +name: ERD to GitHub Pages + +on: + pull_request: + branches: [ "**" ] + paths: [ "**" ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + pull-requests: write + +jobs: + generate-erd-from-flyway: + runs-on: ubuntu-latest + + services: + mysql: + image: mysql:8.0 + env: + MYSQL_USER: ${{ secrets.TEST_DB_USER }} + MYSQL_PASSWORD: ${{ secrets.TEST_DB_PASSWORD }} + MYSQL_ROOT_PASSWORD: ${{ secrets.TEST_DB_ROOT_PASSWORD }} + MYSQL_DATABASE: testdb + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Download Flyway + run: | + curl -sSL "https://repo1.maven.org/maven2/org/flywaydb/flyway-commandline/10.13.0/flyway-commandline-10.13.0-linux-x64.tar.gz" -o flyway.tar.gz + tar -xzf flyway.tar.gz + echo "$(pwd)/flyway-10.13.0" >> $GITHUB_PATH + + - name: Run Flyway migrations + run: | + flyway -url="jdbc:mysql://127.0.0.1:3306/testdb?allowPublicKeyRetrieval=true&useSSL=false" -user=${{ secrets.TEST_DB_USER }} -password=${{ secrets.TEST_DB_PASSWORD }} -locations=filesystem:src/main/resources/db/migration migrate + shell: bash + + - name: Dump database schema + run: | + mkdir -p erd + mysqldump -h 127.0.0.1 -u ${{ secrets.TEST_DB_USER }} -p${{ secrets.TEST_DB_PASSWORD }} --no-data --skip-comments testdb > erd/schema.sql + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install DBML Tools + run: npm install -g @dbml/cli @softwaretechnik/dbml-renderer + + - name: Convert SQL to DBML + run: | + npx sql2dbml erd/schema.sql --mysql -o erd/schema.dbml + + - name: Render ERD to SVG + run: | + npx dbml-renderer -i erd/schema.dbml -o erd/erd.svg + + - name: Generate HTML wrapper + run: | + echo "ERD Preview

ERD Preview for PR #${{ github.event.pull_request.number }}

" > erd/index.html + + - name: Deploy ERD to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./erd + force_orphan: true + + - name: Comment on PR with ERD Link + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ github.event.pull_request.number }} + body: | + ๐Ÿ“Œ ์ตœ์‹  ERD๊ฐ€ ์ž๋™ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + + ๐Ÿ‘‰ [ERD ๋ณด๋Ÿฌ๊ฐ€๊ธฐ](https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/) diff --git a/build.gradle b/build.gradle index 50c942db..20ee0587 100644 --- a/build.gradle +++ b/build.gradle @@ -27,8 +27,8 @@ jacoco { sonarqube { properties { - property "sonar.projectKey", "baegam_timeeat" - property "sonar.organization", "baegam" + property "sonar.projectKey", "YAPP-Github_26th-Web-Team-1-BE" + property "sonar.organization", "yapp-github" property "sonar.host.url", "https://sonarcloud.io" property 'sonar.sourceEncoding', 'UTF-8' property 'sonar.java.coveragePlugin', 'jacoco' @@ -56,6 +56,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' // Lombok compileOnly 'org.projectlombok:lombok' @@ -85,6 +86,8 @@ dependencies { // aws implementation 'io.awspring.cloud:spring-cloud-aws-starter-parameter-store:3.2.1' + implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.2.1' + implementation 'software.amazon.awssdk:s3:2.31.77' } bootJar { @@ -125,18 +128,18 @@ generateSwaggerUI { openapi3 { servers = [ - { - url = "https://api-dev.eatda.net" - description = "Develop Server" - }, - { - url = "https://api.eatda.net" - description = "Production Server" - }, - { - url = "http://localhost:8080" - description = "Local Server" - } + { + url = "https://api-dev.eatda.net" + description = "Develop Server" + }, + { + url = "https://api.eatda.net" + description = "Production Server" + }, + { + url = "http://localhost:8080" + description = "Local Server" + } ] title = "EatDa API" description = "EatDa API ๋ช…์„ธ์„œ" diff --git a/src/main/java/timeeat/TimeEatApplication.java b/src/main/java/eatda/EatdaApplication.java similarity index 63% rename from src/main/java/timeeat/TimeEatApplication.java rename to src/main/java/eatda/EatdaApplication.java index dffe973b..36d4802f 100644 --- a/src/main/java/timeeat/TimeEatApplication.java +++ b/src/main/java/eatda/EatdaApplication.java @@ -1,12 +1,12 @@ -package timeeat; +package eatda; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class TimeEatApplication { +public class EatdaApplication { public static void main(String[] args) { - SpringApplication.run(TimeEatApplication.class, args); + SpringApplication.run(EatdaApplication.class, args); } } diff --git a/src/main/java/eatda/client/map/KakaoProperties.java b/src/main/java/eatda/client/map/KakaoProperties.java new file mode 100644 index 00000000..bd1e0a4b --- /dev/null +++ b/src/main/java/eatda/client/map/KakaoProperties.java @@ -0,0 +1,22 @@ +package eatda.client.map; + +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@ConfigurationProperties(prefix = "kakao") +public class KakaoProperties { + + private final String apiKey; + + public KakaoProperties(String apiKey) { + validateApiKey(apiKey); + this.apiKey = apiKey; + } + + private void validateApiKey(String apiKey) { + if (apiKey == null || apiKey.isBlank()) { + throw new IllegalArgumentException("API key must not be null or blank"); + } + } +} diff --git a/src/main/java/eatda/client/map/MapClient.java b/src/main/java/eatda/client/map/MapClient.java new file mode 100644 index 00000000..913460ab --- /dev/null +++ b/src/main/java/eatda/client/map/MapClient.java @@ -0,0 +1,41 @@ +package eatda.client.map; + +import eatda.domain.store.Coordinates; +import java.util.List; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.http.HttpStatusCode; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +@Component +@EnableConfigurationProperties(KakaoProperties.class) +public class MapClient { + + private final RestClient restClient; + private final KakaoProperties kakaoProperties; + + public MapClient(RestClient.Builder restClient, KakaoProperties properties) { + this.restClient = restClient + .defaultStatusHandler(HttpStatusCode::is5xxServerError, new MapServerErrorHandler()) + .build(); + this.kakaoProperties = properties; + } + + public List searchShops(String query) { + return restClient.get() + .uri("https://dapi.kakao.com/v2/local/search/keyword.json", builder -> builder + .queryParam("query", query) + .queryParam("category", "FD6") + .queryParam("rect", "%s,%s,%s,%s".formatted( + Coordinates.getMinLongitude(), Coordinates.getMinLatitude(), + Coordinates.getMaxLongitude(), Coordinates.getMaxLatitude())) + .queryParam("page", 1) + .queryParam("size", 15) + .queryParam("sort", "accuracy") + .build()) + .header("Authorization", "KakaoAK " + kakaoProperties.getApiKey()) + .retrieve() + .body(StoreSearchResults.class) + .results(); + } +} diff --git a/src/main/java/eatda/client/map/MapServerErrorHandler.java b/src/main/java/eatda/client/map/MapServerErrorHandler.java new file mode 100644 index 00000000..6ffd5bff --- /dev/null +++ b/src/main/java/eatda/client/map/MapServerErrorHandler.java @@ -0,0 +1,16 @@ +package eatda.client.map; + +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; +import java.io.IOException; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.web.client.RestClient.ResponseSpec.ErrorHandler; + +public class MapServerErrorHandler implements ErrorHandler { + + @Override + public void handle(HttpRequest request, ClientHttpResponse response) throws IOException { + throw new BusinessException(BusinessErrorCode.MAP_SERVER_ERROR); + } +} diff --git a/src/main/java/eatda/client/map/StoreSearchResult.java b/src/main/java/eatda/client/map/StoreSearchResult.java new file mode 100644 index 00000000..04e22cc0 --- /dev/null +++ b/src/main/java/eatda/client/map/StoreSearchResult.java @@ -0,0 +1,30 @@ +package eatda.client.map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record StoreSearchResult( + @JsonProperty("id") String kakaoId, + @JsonProperty("category_group_code") String categoryGroupCode, + @JsonProperty("category_name") String categoryName, + @JsonProperty("phone") String phoneNumber, + @JsonProperty("place_name") String name, + @JsonProperty("place_url") String placeUrl, + @JsonProperty("address_name") String lotNumberAddress, + @JsonProperty("road_address_name") String roadAddress, + @JsonProperty("y") double latitude, + @JsonProperty("x") double longitude +) { + + public boolean isFoodStore() { + return "FD6".equals(categoryGroupCode); + } + + public boolean isInSeoul() { + if (lotNumberAddress == null || lotNumberAddress.isBlank()) { + return false; + } + return lotNumberAddress.trim().startsWith("์„œ์šธ"); + } +} diff --git a/src/main/java/eatda/client/map/StoreSearchResults.java b/src/main/java/eatda/client/map/StoreSearchResults.java new file mode 100644 index 00000000..2258ab3f --- /dev/null +++ b/src/main/java/eatda/client/map/StoreSearchResults.java @@ -0,0 +1,9 @@ +package eatda.client.map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record StoreSearchResults(@JsonProperty("documents") List results) { +} diff --git a/src/main/java/timeeat/client/oauth/OauthClient.java b/src/main/java/eatda/client/oauth/OauthClient.java similarity index 95% rename from src/main/java/timeeat/client/oauth/OauthClient.java rename to src/main/java/eatda/client/oauth/OauthClient.java index b358d4db..1d1d0c95 100644 --- a/src/main/java/timeeat/client/oauth/OauthClient.java +++ b/src/main/java/eatda/client/oauth/OauthClient.java @@ -1,5 +1,7 @@ -package timeeat.client.oauth; +package eatda.client.oauth; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; import java.net.URI; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.http.HttpStatusCode; @@ -9,8 +11,6 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClient; import org.springframework.web.util.UriComponentsBuilder; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; @Component @EnableConfigurationProperties(OauthProperties.class) diff --git a/src/main/java/timeeat/client/oauth/OauthMemberInformation.java b/src/main/java/eatda/client/oauth/OauthMemberInformation.java similarity index 56% rename from src/main/java/timeeat/client/oauth/OauthMemberInformation.java rename to src/main/java/eatda/client/oauth/OauthMemberInformation.java index 38fc4ed8..2d03035f 100644 --- a/src/main/java/timeeat/client/oauth/OauthMemberInformation.java +++ b/src/main/java/eatda/client/oauth/OauthMemberInformation.java @@ -1,15 +1,15 @@ -package timeeat.client.oauth; +package eatda.client.oauth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import timeeat.domain.member.Member; +import eatda.domain.member.Member; @JsonDeserialize(using = OauthMemberInformationDeserializer.class) @JsonIgnoreProperties(ignoreUnknown = true) -public record OauthMemberInformation(long socialId, String nickname) { +public record OauthMemberInformation(long socialId, String email, String nickname) { public Member toMember() { - return new Member(Long.toString(socialId), nickname); + return new Member(Long.toString(socialId), email, nickname); } } diff --git a/src/main/java/timeeat/client/oauth/OauthMemberInformationDeserializer.java b/src/main/java/eatda/client/oauth/OauthMemberInformationDeserializer.java similarity index 78% rename from src/main/java/timeeat/client/oauth/OauthMemberInformationDeserializer.java rename to src/main/java/eatda/client/oauth/OauthMemberInformationDeserializer.java index da79f5ac..b06692cc 100644 --- a/src/main/java/timeeat/client/oauth/OauthMemberInformationDeserializer.java +++ b/src/main/java/eatda/client/oauth/OauthMemberInformationDeserializer.java @@ -1,10 +1,10 @@ -package timeeat.client.oauth; +package eatda.client.oauth; -import java.io.IOException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; +import java.io.IOException; public class OauthMemberInformationDeserializer extends JsonDeserializer { @@ -14,11 +14,15 @@ public OauthMemberInformation deserialize(JsonParser jsonParser, JsonNode root = jsonParser.getCodec().readTree(jsonParser); long id = root.path("id").asLong(); + String email = root + .path("kakao_account") + .path("email") + .asText(null); String nickname = root .path("kakao_account") .path("profile") .path("nickname") .asText(null); - return new OauthMemberInformation(id, nickname); + return new OauthMemberInformation(id, email, nickname); } } diff --git a/src/main/java/timeeat/client/oauth/OauthProperties.java b/src/main/java/eatda/client/oauth/OauthProperties.java similarity index 96% rename from src/main/java/timeeat/client/oauth/OauthProperties.java rename to src/main/java/eatda/client/oauth/OauthProperties.java index b0b89ecd..857db323 100644 --- a/src/main/java/timeeat/client/oauth/OauthProperties.java +++ b/src/main/java/eatda/client/oauth/OauthProperties.java @@ -1,10 +1,10 @@ -package timeeat.client.oauth; +package eatda.client.oauth; +import eatda.exception.InitializeException; import java.net.URI; import java.util.List; import lombok.Getter; import org.springframework.boot.context.properties.ConfigurationProperties; -import timeeat.exception.InitializeException; @Getter @ConfigurationProperties(prefix = "oauth") diff --git a/src/main/java/timeeat/client/oauth/OauthServerErrorHandler.java b/src/main/java/eatda/client/oauth/OauthServerErrorHandler.java similarity index 81% rename from src/main/java/timeeat/client/oauth/OauthServerErrorHandler.java rename to src/main/java/eatda/client/oauth/OauthServerErrorHandler.java index fdc1bc0b..26fc2ae5 100644 --- a/src/main/java/timeeat/client/oauth/OauthServerErrorHandler.java +++ b/src/main/java/eatda/client/oauth/OauthServerErrorHandler.java @@ -1,12 +1,12 @@ -package timeeat.client.oauth; +package eatda.client.oauth; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; import java.io.IOException; import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient.ResponseSpec.ErrorHandler; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; @Component public class OauthServerErrorHandler implements ErrorHandler { diff --git a/src/main/java/timeeat/client/oauth/OauthToken.java b/src/main/java/eatda/client/oauth/OauthToken.java similarity index 88% rename from src/main/java/timeeat/client/oauth/OauthToken.java rename to src/main/java/eatda/client/oauth/OauthToken.java index 97e8ed07..5cdeeb00 100644 --- a/src/main/java/timeeat/client/oauth/OauthToken.java +++ b/src/main/java/eatda/client/oauth/OauthToken.java @@ -1,4 +1,4 @@ -package timeeat.client.oauth; +package eatda.client.oauth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/timeeat/config/CorsConfig.java b/src/main/java/eatda/config/CorsConfig.java similarity index 95% rename from src/main/java/timeeat/config/CorsConfig.java rename to src/main/java/eatda/config/CorsConfig.java index 81927ff7..a8667be4 100644 --- a/src/main/java/timeeat/config/CorsConfig.java +++ b/src/main/java/eatda/config/CorsConfig.java @@ -1,11 +1,11 @@ -package timeeat.config; +package eatda.config; +import eatda.exception.InitializeException; import java.util.List; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import timeeat.exception.InitializeException; @Configuration public class CorsConfig implements WebMvcConfigurer { diff --git a/src/main/java/timeeat/config/CorsProperties.java b/src/main/java/eatda/config/CorsProperties.java similarity index 93% rename from src/main/java/timeeat/config/CorsProperties.java rename to src/main/java/eatda/config/CorsProperties.java index 636f73ff..7a8eae65 100644 --- a/src/main/java/timeeat/config/CorsProperties.java +++ b/src/main/java/eatda/config/CorsProperties.java @@ -1,10 +1,10 @@ -package timeeat.config; +package eatda.config; import java.util.List; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; import lombok.Getter; import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; @Getter @Setter diff --git a/src/main/java/timeeat/config/WebConfig.java b/src/main/java/eatda/config/WebConfig.java similarity index 81% rename from src/main/java/timeeat/config/WebConfig.java rename to src/main/java/eatda/config/WebConfig.java index e1656c2c..6e6eb52b 100644 --- a/src/main/java/timeeat/config/WebConfig.java +++ b/src/main/java/eatda/config/WebConfig.java @@ -1,12 +1,12 @@ -package timeeat.config; +package eatda.config; +import eatda.controller.web.auth.AuthMemberArgumentResolver; +import eatda.controller.web.jwt.JwtManager; import java.util.List; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import lombok.RequiredArgsConstructor; -import timeeat.controller.web.auth.AuthMemberArgumentResolver; -import timeeat.controller.web.jwt.JwtManager; @Configuration @RequiredArgsConstructor diff --git a/src/main/java/timeeat/controller/auth/AuthController.java b/src/main/java/eatda/controller/auth/AuthController.java similarity index 92% rename from src/main/java/timeeat/controller/auth/AuthController.java rename to src/main/java/eatda/controller/auth/AuthController.java index 1de6f6e6..eb989fd9 100644 --- a/src/main/java/timeeat/controller/auth/AuthController.java +++ b/src/main/java/eatda/controller/auth/AuthController.java @@ -1,5 +1,8 @@ -package timeeat.controller.auth; +package eatda.controller.auth; +import eatda.controller.member.MemberResponse; +import eatda.controller.web.jwt.JwtManager; +import eatda.service.auth.AuthService; import java.net.URI; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; @@ -10,9 +13,6 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; -import timeeat.controller.member.MemberResponse; -import timeeat.controller.web.jwt.JwtManager; -import timeeat.service.auth.AuthService; @RestController @RequiredArgsConstructor diff --git a/src/main/java/timeeat/controller/auth/LoginRequest.java b/src/main/java/eatda/controller/auth/LoginRequest.java similarity index 64% rename from src/main/java/timeeat/controller/auth/LoginRequest.java rename to src/main/java/eatda/controller/auth/LoginRequest.java index 824430fc..9f8fcd0c 100644 --- a/src/main/java/timeeat/controller/auth/LoginRequest.java +++ b/src/main/java/eatda/controller/auth/LoginRequest.java @@ -1,4 +1,4 @@ -package timeeat.controller.auth; +package eatda.controller.auth; public record LoginRequest(String code, String origin) { } diff --git a/src/main/java/timeeat/controller/auth/LoginResponse.java b/src/main/java/eatda/controller/auth/LoginResponse.java similarity index 50% rename from src/main/java/timeeat/controller/auth/LoginResponse.java rename to src/main/java/eatda/controller/auth/LoginResponse.java index 5b6de9b9..5946e4ee 100644 --- a/src/main/java/timeeat/controller/auth/LoginResponse.java +++ b/src/main/java/eatda/controller/auth/LoginResponse.java @@ -1,6 +1,6 @@ -package timeeat.controller.auth; +package eatda.controller.auth; -import timeeat.controller.member.MemberResponse; +import eatda.controller.member.MemberResponse; public record LoginResponse(TokenResponse token, MemberResponse information) { diff --git a/src/main/java/timeeat/controller/auth/ReissueRequest.java b/src/main/java/eatda/controller/auth/ReissueRequest.java similarity index 62% rename from src/main/java/timeeat/controller/auth/ReissueRequest.java rename to src/main/java/eatda/controller/auth/ReissueRequest.java index 82f0dc23..009ff551 100644 --- a/src/main/java/timeeat/controller/auth/ReissueRequest.java +++ b/src/main/java/eatda/controller/auth/ReissueRequest.java @@ -1,4 +1,4 @@ -package timeeat.controller.auth; +package eatda.controller.auth; public record ReissueRequest(String refreshToken) { } diff --git a/src/main/java/timeeat/controller/auth/TokenResponse.java b/src/main/java/eatda/controller/auth/TokenResponse.java similarity index 69% rename from src/main/java/timeeat/controller/auth/TokenResponse.java rename to src/main/java/eatda/controller/auth/TokenResponse.java index e6e2f916..4786c290 100644 --- a/src/main/java/timeeat/controller/auth/TokenResponse.java +++ b/src/main/java/eatda/controller/auth/TokenResponse.java @@ -1,4 +1,4 @@ -package timeeat.controller.auth; +package eatda.controller.auth; public record TokenResponse(String accessToken, String refreshToken) { } diff --git a/src/main/java/timeeat/controller/member/MemberController.java b/src/main/java/eatda/controller/member/MemberController.java similarity index 80% rename from src/main/java/timeeat/controller/member/MemberController.java rename to src/main/java/eatda/controller/member/MemberController.java index 00bd53e1..9f6daf4f 100644 --- a/src/main/java/timeeat/controller/member/MemberController.java +++ b/src/main/java/eatda/controller/member/MemberController.java @@ -1,5 +1,7 @@ -package timeeat.controller.member; +package eatda.controller.member; +import eatda.controller.web.auth.LoginMember; +import eatda.service.member.MemberService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -8,8 +10,6 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import timeeat.controller.web.auth.LoginMember; -import timeeat.service.service.MemberService; @RestController @RequiredArgsConstructor @@ -17,6 +17,12 @@ public class MemberController { private final MemberService memberService; + @GetMapping("/api/member") + public ResponseEntity getMember(LoginMember member) { + MemberResponse response = memberService.getMember(member.id()); + return ResponseEntity.ok(response); + } + @GetMapping("/api/member/nickname/check") public ResponseEntity checkNickname(LoginMember member, @RequestParam String nickname) { memberService.validateNickname(nickname, member.id()); diff --git a/src/main/java/timeeat/controller/member/MemberResponse.java b/src/main/java/eatda/controller/member/MemberResponse.java similarity index 77% rename from src/main/java/timeeat/controller/member/MemberResponse.java rename to src/main/java/eatda/controller/member/MemberResponse.java index 45fc302f..91274c27 100644 --- a/src/main/java/timeeat/controller/member/MemberResponse.java +++ b/src/main/java/eatda/controller/member/MemberResponse.java @@ -1,21 +1,21 @@ -package timeeat.controller.member; +package eatda.controller.member; -import timeeat.domain.member.Member; +import eatda.domain.member.Member; public record MemberResponse(long id, + String email, boolean isSignUp, String nickname, String phoneNumber, - String interestArea, Boolean optInMarketing ) { public MemberResponse(Member member, boolean isSignUp) { this(member.getId(), + member.getEmail(), isSignUp, member.getNickname(), member.getPhoneNumber(), - member.getInterestAreaName(), member.getOptInMarketing()); } diff --git a/src/main/java/timeeat/controller/member/MemberUpdateRequest.java b/src/main/java/eatda/controller/member/MemberUpdateRequest.java similarity index 56% rename from src/main/java/timeeat/controller/member/MemberUpdateRequest.java rename to src/main/java/eatda/controller/member/MemberUpdateRequest.java index 028938f9..ec8a893c 100644 --- a/src/main/java/timeeat/controller/member/MemberUpdateRequest.java +++ b/src/main/java/eatda/controller/member/MemberUpdateRequest.java @@ -1,14 +1,13 @@ -package timeeat.controller.member; +package eatda.controller.member; +import eatda.domain.member.Member; import jakarta.validation.constraints.NotBlank; -import timeeat.domain.member.Member; public record MemberUpdateRequest(@NotBlank String nickname, @NotBlank String phoneNumber, - @NotBlank String interestArea, boolean optInMarketing) { public Member toMemberUpdater() { - return new Member(nickname, phoneNumber, interestArea, optInMarketing); + return new Member(nickname, phoneNumber, optInMarketing); } } diff --git a/src/main/java/eatda/controller/store/CheerController.java b/src/main/java/eatda/controller/store/CheerController.java new file mode 100644 index 00000000..b2fc61a4 --- /dev/null +++ b/src/main/java/eatda/controller/store/CheerController.java @@ -0,0 +1,23 @@ +package eatda.controller.store; + +import eatda.service.store.CheerService; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class CheerController { + + private final CheerService cheerService; + + @GetMapping("/api/cheer") + public ResponseEntity getCheers(@RequestParam @Min(1) @Max(50) int size) { + CheersResponse response = cheerService.getCheers(size); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/eatda/controller/store/CheerPreviewResponse.java b/src/main/java/eatda/controller/store/CheerPreviewResponse.java new file mode 100644 index 00000000..73525aef --- /dev/null +++ b/src/main/java/eatda/controller/store/CheerPreviewResponse.java @@ -0,0 +1,29 @@ +package eatda.controller.store; + +import eatda.domain.store.Cheer; +import eatda.domain.store.Store; + +public record CheerPreviewResponse( + long storeId, + String imageUrl, + String storeName, + String storeDistrict, + String storeNeighborhood, + String storeCategory, + long cheerId, + String cheerDescription +) { + + public CheerPreviewResponse(Cheer cheer, Store store, String imageUrl) { + this( + store.getId(), + imageUrl, + store.getName(), + store.getAddressDistrict(), + store.getAddressNeighborhood(), + store.getCategory().getCategoryName(), + cheer.getId(), + cheer.getDescription() + ); + } +} diff --git a/src/main/java/eatda/controller/store/CheersResponse.java b/src/main/java/eatda/controller/store/CheersResponse.java new file mode 100644 index 00000000..2ad034e5 --- /dev/null +++ b/src/main/java/eatda/controller/store/CheersResponse.java @@ -0,0 +1,7 @@ +package eatda.controller.store; + +import java.util.List; + +public record CheersResponse(List cheers) { + +} diff --git a/src/main/java/eatda/controller/store/StoreController.java b/src/main/java/eatda/controller/store/StoreController.java new file mode 100644 index 00000000..09d8e0a9 --- /dev/null +++ b/src/main/java/eatda/controller/store/StoreController.java @@ -0,0 +1,29 @@ +package eatda.controller.store; + +import eatda.controller.web.auth.LoginMember; +import eatda.service.store.StoreService; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class StoreController { + + private final StoreService storeService; + + @GetMapping("/api/shops") + public ResponseEntity getStores(@RequestParam @Min(1) @Max(50) int size) { + return ResponseEntity.ok(storeService.getStores(size)); + } + + @GetMapping("/api/shop/search") + public ResponseEntity searchStore(@RequestParam String query, LoginMember member) { + StoreSearchResponses response = storeService.searchStores(query); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/eatda/controller/store/StorePreviewResponse.java b/src/main/java/eatda/controller/store/StorePreviewResponse.java new file mode 100644 index 00000000..00a6e18f --- /dev/null +++ b/src/main/java/eatda/controller/store/StorePreviewResponse.java @@ -0,0 +1,24 @@ +package eatda.controller.store; + +import eatda.domain.store.Store; + +public record StorePreviewResponse( + long id, + String imageUrl, + String name, + String district, + String neighborhood, + String category +) { + + public StorePreviewResponse(Store store, String imageUrl) { + this( + store.getId(), + imageUrl, + store.getName(), + store.getAddressDistrict(), + store.getAddressNeighborhood(), + store.getCategory().getCategoryName() + ); + } +} diff --git a/src/main/java/eatda/controller/store/StoreSearchResponse.java b/src/main/java/eatda/controller/store/StoreSearchResponse.java new file mode 100644 index 00000000..bc1ae03a --- /dev/null +++ b/src/main/java/eatda/controller/store/StoreSearchResponse.java @@ -0,0 +1,18 @@ +package eatda.controller.store; + +import eatda.client.map.StoreSearchResult; + +public record StoreSearchResponse( + String kakaoId, + String name, + String address +) { + + public StoreSearchResponse(StoreSearchResult searchResult) { + this( + searchResult.kakaoId(), + searchResult.name(), + searchResult.lotNumberAddress() + ); + } +} diff --git a/src/main/java/eatda/controller/store/StoreSearchResponses.java b/src/main/java/eatda/controller/store/StoreSearchResponses.java new file mode 100644 index 00000000..6b34ef61 --- /dev/null +++ b/src/main/java/eatda/controller/store/StoreSearchResponses.java @@ -0,0 +1,14 @@ +package eatda.controller.store; + +import eatda.client.map.StoreSearchResult; +import java.util.List; + +public record StoreSearchResponses(List stores) { + + public static StoreSearchResponses from(List searchResults) { + List storeResponses = searchResults.stream() + .map(StoreSearchResponse::new) + .toList(); + return new StoreSearchResponses(storeResponses); + } +} diff --git a/src/main/java/eatda/controller/store/StoresResponse.java b/src/main/java/eatda/controller/store/StoresResponse.java new file mode 100644 index 00000000..a5652aab --- /dev/null +++ b/src/main/java/eatda/controller/store/StoresResponse.java @@ -0,0 +1,6 @@ +package eatda.controller.store; + +import java.util.List; + +public record StoresResponse(List stores) { +} diff --git a/src/main/java/eatda/controller/story/FilteredSearchResult.java b/src/main/java/eatda/controller/story/FilteredSearchResult.java new file mode 100644 index 00000000..89d94efe --- /dev/null +++ b/src/main/java/eatda/controller/story/FilteredSearchResult.java @@ -0,0 +1,9 @@ +package eatda.controller.story; + +public record FilteredSearchResult( + String kakaoId, + String name, + String address, + String category +) { +} diff --git a/src/main/java/eatda/controller/story/StoriesResponse.java b/src/main/java/eatda/controller/story/StoriesResponse.java new file mode 100644 index 00000000..8829449e --- /dev/null +++ b/src/main/java/eatda/controller/story/StoriesResponse.java @@ -0,0 +1,13 @@ +package eatda.controller.story; + +import java.util.List; + +public record StoriesResponse( + List stories +) { + public record StoryPreview( + Long storyId, + String imageUrl + ) { + } +} diff --git a/src/main/java/eatda/controller/story/StoryController.java b/src/main/java/eatda/controller/story/StoryController.java new file mode 100644 index 00000000..382ae3d4 --- /dev/null +++ b/src/main/java/eatda/controller/story/StoryController.java @@ -0,0 +1,28 @@ +package eatda.controller.story; + +import eatda.controller.web.auth.LoginMember; +import eatda.service.story.StoryService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequiredArgsConstructor +public class StoryController { + + private final StoryService storyService; + + @PostMapping("/api/stories") + public ResponseEntity registerStory( + @RequestPart("request") StoryRegisterRequest request, + @RequestPart("image") MultipartFile image, + LoginMember member + ) { + storyService.registerStory(request, image, member.id()); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } +} diff --git a/src/main/java/eatda/controller/story/StoryRegisterRequest.java b/src/main/java/eatda/controller/story/StoryRegisterRequest.java new file mode 100644 index 00000000..aeebfd4e --- /dev/null +++ b/src/main/java/eatda/controller/story/StoryRegisterRequest.java @@ -0,0 +1,8 @@ +package eatda.controller.story; + +public record StoryRegisterRequest( + String query, + String storeKakaoId, + String description +) { +} diff --git a/src/main/java/eatda/controller/story/StoryResponse.java b/src/main/java/eatda/controller/story/StoryResponse.java new file mode 100644 index 00000000..df46d476 --- /dev/null +++ b/src/main/java/eatda/controller/story/StoryResponse.java @@ -0,0 +1,11 @@ +package eatda.controller.story; + +public record StoryResponse( + String storeKakaoId, + String category, + String storeName, + String storeAddress, + String description, + String imageUrl +) { +} diff --git a/src/main/java/timeeat/controller/web/auth/AuthMemberArgumentResolver.java b/src/main/java/eatda/controller/web/auth/AuthMemberArgumentResolver.java similarity index 88% rename from src/main/java/timeeat/controller/web/auth/AuthMemberArgumentResolver.java rename to src/main/java/eatda/controller/web/auth/AuthMemberArgumentResolver.java index 6be3ea32..b74fd6a5 100644 --- a/src/main/java/timeeat/controller/web/auth/AuthMemberArgumentResolver.java +++ b/src/main/java/eatda/controller/web/auth/AuthMemberArgumentResolver.java @@ -1,5 +1,8 @@ -package timeeat.controller.web.auth; +package eatda.controller.web.auth; +import eatda.controller.web.jwt.JwtManager; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; import org.springframework.http.HttpHeaders; @@ -7,9 +10,6 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; -import timeeat.controller.web.jwt.JwtManager; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; @RequiredArgsConstructor public class AuthMemberArgumentResolver implements HandlerMethodArgumentResolver { diff --git a/src/main/java/timeeat/controller/web/auth/LoginMember.java b/src/main/java/eatda/controller/web/auth/LoginMember.java similarity index 51% rename from src/main/java/timeeat/controller/web/auth/LoginMember.java rename to src/main/java/eatda/controller/web/auth/LoginMember.java index e50c00d8..ab9d7403 100644 --- a/src/main/java/timeeat/controller/web/auth/LoginMember.java +++ b/src/main/java/eatda/controller/web/auth/LoginMember.java @@ -1,4 +1,4 @@ -package timeeat.controller.web.auth; +package eatda.controller.web.auth; public record LoginMember(long id) { } diff --git a/src/main/java/timeeat/controller/web/jwt/JwtManager.java b/src/main/java/eatda/controller/web/jwt/JwtManager.java similarity index 95% rename from src/main/java/timeeat/controller/web/jwt/JwtManager.java rename to src/main/java/eatda/controller/web/jwt/JwtManager.java index b48b2d4b..c0be0305 100644 --- a/src/main/java/timeeat/controller/web/jwt/JwtManager.java +++ b/src/main/java/eatda/controller/web/jwt/JwtManager.java @@ -1,14 +1,14 @@ -package timeeat.controller.web.jwt; +package eatda.controller.web.jwt; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; import java.time.Duration; import java.util.Date; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.stereotype.Component; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Jwts; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; @Component @EnableConfigurationProperties(JwtProperties.class) diff --git a/src/main/java/timeeat/controller/web/jwt/JwtProperties.java b/src/main/java/eatda/controller/web/jwt/JwtProperties.java similarity index 95% rename from src/main/java/timeeat/controller/web/jwt/JwtProperties.java rename to src/main/java/eatda/controller/web/jwt/JwtProperties.java index b294d767..d41e13be 100644 --- a/src/main/java/timeeat/controller/web/jwt/JwtProperties.java +++ b/src/main/java/eatda/controller/web/jwt/JwtProperties.java @@ -1,12 +1,12 @@ -package timeeat.controller.web.jwt; +package eatda.controller.web.jwt; +import eatda.exception.InitializeException; import io.jsonwebtoken.security.Keys; import java.time.Duration; import java.util.Base64; import javax.crypto.SecretKey; import lombok.Getter; import org.springframework.boot.context.properties.ConfigurationProperties; -import timeeat.exception.InitializeException; @Getter @ConfigurationProperties(prefix = "jwt") diff --git a/src/main/java/timeeat/controller/web/jwt/TokenType.java b/src/main/java/eatda/controller/web/jwt/TokenType.java similarity index 63% rename from src/main/java/timeeat/controller/web/jwt/TokenType.java rename to src/main/java/eatda/controller/web/jwt/TokenType.java index 9aaf522e..7397d0bd 100644 --- a/src/main/java/timeeat/controller/web/jwt/TokenType.java +++ b/src/main/java/eatda/controller/web/jwt/TokenType.java @@ -1,4 +1,4 @@ -package timeeat.controller.web.jwt; +package eatda.controller.web.jwt; public enum TokenType { ACCESS_TOKEN, diff --git a/src/main/java/eatda/domain/AuditingEntity.java b/src/main/java/eatda/domain/AuditingEntity.java new file mode 100644 index 00000000..bac7a511 --- /dev/null +++ b/src/main/java/eatda/domain/AuditingEntity.java @@ -0,0 +1,20 @@ +package eatda.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.PrePersist; +import java.time.LocalDateTime; +import lombok.Getter; + +@Getter +@MappedSuperclass +public abstract class AuditingEntity { + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/eatda/domain/article/Article.java b/src/main/java/eatda/domain/article/Article.java new file mode 100644 index 00000000..9e59e72a --- /dev/null +++ b/src/main/java/eatda/domain/article/Article.java @@ -0,0 +1,42 @@ +package eatda.domain.article; + +import eatda.domain.AuditingEntity; +import jakarta.persistence.Column; +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.Getter; +import lombok.NoArgsConstructor; + +@Table(name = "article") +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Article extends AuditingEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String subtitle; + + @Column(name = "article_url", nullable = false, length = 511) + private String articleUrl; + + @Column(name = "image_key", nullable = false, length = 511) + private String imageKey; + + public Article(String title, String subtitle, String articleUrl, String imageKey) { + this.title = title; + this.subtitle = subtitle; + this.articleUrl = articleUrl; + this.imageKey = imageKey; + } +} diff --git a/src/main/java/timeeat/domain/member/Member.java b/src/main/java/eatda/domain/member/Member.java similarity index 74% rename from src/main/java/timeeat/domain/member/Member.java rename to src/main/java/eatda/domain/member/Member.java index 6a395090..29d5a138 100644 --- a/src/main/java/timeeat/domain/member/Member.java +++ b/src/main/java/eatda/domain/member/Member.java @@ -1,27 +1,28 @@ -package timeeat.domain.member; +package eatda.domain.member; +import eatda.domain.AuditingEntity; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; import jakarta.annotation.Nullable; import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; +import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import timeeat.enums.InterestArea; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; @Table(name = "member") @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Member { +public class Member extends AuditingEntity { + + private static final String EMAIL_REGEX = "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -30,43 +31,43 @@ public class Member { @Column(name = "social_id", unique = true, nullable = false) private String socialId; + @Column(name = "email", unique = true, nullable = false) + private String email; + @Column(name = "nickname") private String nickname; @Embedded private MobilePhoneNumber mobilePhoneNumber; - @Enumerated(EnumType.STRING) - @Column(name = "interest_area") - private InterestArea interestArea; - @Column(name = "opt_in_marketing") private Boolean optInMarketing; - public Member(String socialId, String nickname) { + public Member(String socialId, String email, String nickname) { validateSocialId(socialId); + validateEmail(email); this.socialId = socialId; + this.email = email; this.nickname = nickname; } public Member( String socialId, + String email, String nickname, String mobilePhoneNumber, - String interestArea, Boolean optInMarketing ) { - this(socialId, nickname); + this(socialId, email, nickname); validateOptInMarketing(optInMarketing); this.mobilePhoneNumber = new MobilePhoneNumber(mobilePhoneNumber); - this.interestArea = InterestArea.from(interestArea); this.optInMarketing = optInMarketing; } - public Member(String nickname, String mobilePhoneNumber, String interestArea, boolean optInMarketing) { + public Member(String nickname, String mobilePhoneNumber, boolean optInMarketing) { + validateOptInMarketing(optInMarketing); this.nickname = nickname; this.mobilePhoneNumber = new MobilePhoneNumber(mobilePhoneNumber); - this.interestArea = InterestArea.from(interestArea); this.optInMarketing = optInMarketing; } @@ -76,6 +77,12 @@ private void validateSocialId(String socialId) { } } + private void validateEmail(String email) { + if (email == null || !email.matches(EMAIL_REGEX)) { + throw new BusinessException(BusinessErrorCode.INVALID_EMAIL); + } + } + private void validateOptInMarketing(Boolean optInMarketing) { if (optInMarketing == null) { throw new BusinessException(BusinessErrorCode.INVALID_MARKETING_CONSENT); @@ -85,7 +92,6 @@ private void validateOptInMarketing(Boolean optInMarketing) { public void update(Member member) { this.nickname = member.nickname; this.mobilePhoneNumber = member.mobilePhoneNumber; - this.interestArea = member.interestArea; this.optInMarketing = member.optInMarketing; } @@ -111,13 +117,5 @@ public String getPhoneNumber() { } return mobilePhoneNumber.getValue(); } - - @Nullable - public String getInterestAreaName() { - if (interestArea == null) { - return null; - } - return interestArea.getAreaName(); - } } diff --git a/src/main/java/timeeat/domain/member/MobilePhoneNumber.java b/src/main/java/eatda/domain/member/MobilePhoneNumber.java similarity index 88% rename from src/main/java/timeeat/domain/member/MobilePhoneNumber.java rename to src/main/java/eatda/domain/member/MobilePhoneNumber.java index f1db966f..dab42c46 100644 --- a/src/main/java/timeeat/domain/member/MobilePhoneNumber.java +++ b/src/main/java/eatda/domain/member/MobilePhoneNumber.java @@ -1,14 +1,14 @@ -package timeeat.domain.member; +package eatda.domain.member; -import java.util.regex.Pattern; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; import jakarta.persistence.Column; import jakarta.persistence.Embeddable; +import java.util.regex.Pattern; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; @Getter @Embeddable diff --git a/src/main/java/eatda/domain/store/Cheer.java b/src/main/java/eatda/domain/store/Cheer.java new file mode 100644 index 00000000..9339dc4d --- /dev/null +++ b/src/main/java/eatda/domain/store/Cheer.java @@ -0,0 +1,74 @@ +package eatda.domain.store; + +import eatda.domain.AuditingEntity; +import eatda.domain.member.Member; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Table(name = "cheer") +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Cheer extends AuditingEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "store_id", nullable = false) + private Store store; + + @Column(nullable = false, columnDefinition = "TEXT") + private String description; + + @Column(name = "image_key", length = 511) + private String imageKey; + + @Column(name = "is_admin", nullable = false) + private boolean isAdmin; + + public Cheer(Member member, Store store, String description, String imageKey) { + validateDescription(description); + validateImageKey(imageKey); + this.member = member; + this.store = store; + this.description = description; + this.imageKey = imageKey; + + this.isAdmin = false; + } + + public Cheer(Member member, Store store, String description, String imageKey, boolean isAdmin) { + this(member, store, description, imageKey); + this.isAdmin = isAdmin; + } + + private void validateDescription(String description) { + if (description == null || description.isBlank()) { + throw new BusinessException(BusinessErrorCode.INVALID_CHEER_DESCRIPTION); + } + } + + private void validateImageKey(String imageKey) { + if (imageKey != null && imageKey.isBlank()) { + throw new BusinessException(BusinessErrorCode.INVALID_CHEER_IMAGE_KEY); + } + } +} diff --git a/src/main/java/timeeat/domain/store/Coordinates.java b/src/main/java/eatda/domain/store/Coordinates.java similarity index 78% rename from src/main/java/timeeat/domain/store/Coordinates.java rename to src/main/java/eatda/domain/store/Coordinates.java index efbfa87d..57a07c66 100644 --- a/src/main/java/timeeat/domain/store/Coordinates.java +++ b/src/main/java/eatda/domain/store/Coordinates.java @@ -1,13 +1,13 @@ -package timeeat.domain.store; +package eatda.domain.store; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; @Getter @Embeddable @@ -19,6 +19,23 @@ public class Coordinates { private static final double MAX_LATITUDE = 37.715133; private static final double MIN_LONGITUDE = 126.734086; private static final double MAX_LONGITUDE = 127.269311; + + public static double getMinLatitude() { + return MIN_LATITUDE; + } + + public static double getMaxLatitude() { + return MAX_LATITUDE; + } + + public static double getMinLongitude() { + return MIN_LONGITUDE; + } + + public static double getMaxLongitude() { + return MAX_LONGITUDE; + } + @Column(nullable = false) private Double latitude; diff --git a/src/main/java/eatda/domain/store/Store.java b/src/main/java/eatda/domain/store/Store.java new file mode 100644 index 00000000..37e804f5 --- /dev/null +++ b/src/main/java/eatda/domain/store/Store.java @@ -0,0 +1,89 @@ +package eatda.domain.store; + +import eatda.domain.AuditingEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Table(name = "store") +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Store extends AuditingEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "kakao_id", unique = true, nullable = false) + private String kakaoId; + + @Enumerated(EnumType.STRING) + @Column(name = "category", nullable = false) + private StoreCategory category; + + @Column(name = "phone_number", nullable = false) + private String phoneNumber; + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "place_url", nullable = false) + private String placeUrl; + + @Column(name = "road_address", nullable = false) + private String roadAddress; + + @Column(name = "lot_number_address", nullable = false) + private String lotNumberAddress; + + @Embedded + private Coordinates coordinates; + + @Builder + private Store(String kakaoId, + StoreCategory category, + String phoneNumber, + String name, + String placeUrl, + String roadAddress, + String lotNumberAddress, + Double latitude, + Double longitude) { + this.kakaoId = kakaoId; + this.category = category; + this.phoneNumber = phoneNumber; + this.name = name; + this.placeUrl = placeUrl; + this.roadAddress = roadAddress; + this.lotNumberAddress = lotNumberAddress; + this.coordinates = new Coordinates(latitude, longitude); + } + + public String getAddressDistrict() { + String[] addressParts = lotNumberAddress.split(" "); + if (addressParts.length < 2) { + return ""; + } + return addressParts[1]; + } + + public String getAddressNeighborhood() { + String[] addressParts = lotNumberAddress.split(" "); + if (addressParts.length < 3) { + return ""; + } + return addressParts[2]; + } +} diff --git a/src/main/java/timeeat/enums/StoreCategory.java b/src/main/java/eatda/domain/store/StoreCategory.java similarity index 90% rename from src/main/java/timeeat/enums/StoreCategory.java rename to src/main/java/eatda/domain/store/StoreCategory.java index c5687bcc..c2cafe12 100644 --- a/src/main/java/timeeat/enums/StoreCategory.java +++ b/src/main/java/eatda/domain/store/StoreCategory.java @@ -1,9 +1,9 @@ -package timeeat.enums; +package eatda.domain.store; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; import java.util.Arrays; import lombok.Getter; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; @Getter public enum StoreCategory { diff --git a/src/main/java/eatda/domain/story/Story.java b/src/main/java/eatda/domain/story/Story.java new file mode 100644 index 00000000..97817acf --- /dev/null +++ b/src/main/java/eatda/domain/story/Story.java @@ -0,0 +1,129 @@ +package eatda.domain.story; + +import eatda.domain.AuditingEntity; +import eatda.domain.member.Member; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Table(name = "story") +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Story extends AuditingEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @Column(name = "store_kakao_id", nullable = false) + private String storeKakaoId; + + @Column(name = "store_name", nullable = false) + private String storeName; + + @Column(name = "store_address", nullable = false) + private String storeAddress; + + @Column(name = "store_category", nullable = false) + private String storeCategory; + + @Column(name = "description", nullable = false) + private String description; + + @Column(name = "image_key", nullable = false) + private String imageKey; + + @Builder + private Story( + Member member, + String storeKakaoId, + String storeName, + String storeAddress, + String storeCategory, + String description, + String imageKey + ) { + validateMember(member); + validateStore(storeKakaoId, storeName, storeAddress, storeCategory); + validateStory(description, imageKey); + + this.member = member; + this.storeKakaoId = storeKakaoId; + this.storeName = storeName; + this.storeAddress = storeAddress; + this.storeCategory = storeCategory; + this.description = description; + this.imageKey = imageKey; + } + + private void validateMember(Member member) { + if (member == null) { + throw new BusinessException(BusinessErrorCode.STORY_MEMBER_REQUIRED); + } + } + + private void validateStore(String storeKakaoId, String storeName, String storeAddress, String storeCategory) { + validateStoreKakaoId(storeKakaoId); + validateStoreName(storeName); + validateStoreAddress(storeAddress); + validateStoreCategory(storeCategory); + } + + private void validateStory(String description, String imageKey) { + validateDescription(description); + validateImage(imageKey); + } + + private void validateStoreKakaoId(String storeKakaoId) { + if (storeKakaoId == null || storeKakaoId.isBlank()) { + throw new BusinessException(BusinessErrorCode.INVALID_STORE_KAKAO_ID); + } + } + + private void validateStoreName(String storeName) { + if (storeName == null || storeName.isBlank()) { + throw new BusinessException(BusinessErrorCode.INVALID_STORE_NAME); + } + } + + private void validateStoreAddress(String storeAddress) { + if (storeAddress == null || storeAddress.isBlank()) { + throw new BusinessException(BusinessErrorCode.INVALID_STORE_ADDRESS); + } + } + + private void validateStoreCategory(String storeCategory) { + if (storeCategory == null || storeCategory.isBlank()) { + throw new BusinessException(BusinessErrorCode.INVALID_STORE_CATEGORY); + } + } + + private void validateDescription(String description) { + if (description == null || description.isBlank()) { + throw new BusinessException(BusinessErrorCode.INVALID_STORY_DESCRIPTION); + } + } + + private void validateImage(String imageKey) { + if (imageKey == null || imageKey.isBlank()) { + throw new BusinessException(BusinessErrorCode.INVALID_STORY_IMAGE_KEY); + } + } +} diff --git a/src/main/java/timeeat/exception/BusinessErrorCode.java b/src/main/java/eatda/exception/BusinessErrorCode.java similarity index 52% rename from src/main/java/timeeat/exception/BusinessErrorCode.java rename to src/main/java/eatda/exception/BusinessErrorCode.java index 3369eb26..fea49646 100644 --- a/src/main/java/timeeat/exception/BusinessErrorCode.java +++ b/src/main/java/eatda/exception/BusinessErrorCode.java @@ -1,4 +1,4 @@ -package timeeat.exception; +package eatda.exception; import lombok.Getter; import org.springframework.http.HttpStatus; @@ -14,39 +14,44 @@ public enum BusinessErrorCode { INVALID_SOCIAL_ID("MEM005", "์†Œ์…œ ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."), DUPLICATE_NICKNAME("MEM006", "์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ ๋‹‰๋„ค์ž„์ž…๋‹ˆ๋‹ค."), DUPLICATE_PHONE_NUMBER("MEM007", "์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ ์ „ํ™”๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค."), + INVALID_EMAIL("MEM008", "์œ ํšจํ•˜์ง€ ์•Š์€ ์ด๋ฉ”์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค."), // Store INVALID_STORE_CATEGORY("STO001", "์œ ํšจํ•˜์ง€ ์•Š์€ ๋งค์žฅ ์นดํ…Œ๊ณ ๋ฆฌ์ž…๋‹ˆ๋‹ค."), - INVALID_STORE_NAME("STO002", "๋งค์žฅ๋ช…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."), - INVALID_STORE_ADDRESS("STO003", "๋งค์žฅ ์ฃผ์†Œ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."), - INVALID_STORE_PHONE_NUMBER("STO004", "๋งค์žฅ ์ „ํ™”๋ฒˆํ˜ธ๋Š” 9~12์ž๋ฆฌ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - INVALID_STORE_COORDINATES("STO005", "์œ ํšจํ•˜์ง€ ์•Š์€ ์ขŒํ‘œ์ž…๋‹ˆ๋‹ค."), - INVALID_STORE_ID("STO006", "์œ ํšจํ•˜์ง€ ์•Š์€ ๋งค์žฅ ID์ž…๋‹ˆ๋‹ค."), INVALID_STORE_COORDINATES_NULL("STO007", "์ขŒํ‘œ ๊ฐ’์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."), - INVALID_STORE_TIME_NULL("STO008", "์˜์—… ์‹œ๊ฐ„์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."), - INVALID_STORE_TIME_ORDER("STO009", "์ข…๋ฃŒ ์‹œ๊ฐ„์€ ์‹œ์ž‘ ์‹œ๊ฐ„๋ณด๋‹ค ๋Šฆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), OUT_OF_SEOUL_LATITUDE_RANGE("STO010", "์„œ๋น„์Šค ์ง€์—ญ(์„œ์šธ)์„ ๋ฒ—์–ด๋‚œ ์œ„๋„ ๊ฐ’์ž…๋‹ˆ๋‹ค."), OUT_OF_SEOUL_LONGITUDE_RANGE("STO011", "์„œ๋น„์Šค ์ง€์—ญ(์„œ์šธ)์„ ๋ฒ—์–ด๋‚œ ๊ฒฝ๋„ ๊ฐ’์ž…๋‹ˆ๋‹ค."), + STORE_NOT_FOUND("ST0012", "ํ•ด๋‹น ๊ฐ€๊ฒŒ ์ •๋ณด๋ฅผ ์ฐพ์„์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), - // Menu - INVALID_MENU_NAME("MEN001", "๋ฉ”๋‰ด๋ช…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."), - INVALID_MENU_PRICE("MEN002", "๋ฉ”๋‰ด ๊ฐ€๊ฒฉ์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - INVALID_MENU_DISCOUNT_PRICE("MEN003", "ํ• ์ธ ๊ฐ€๊ฒฉ์€ ์›๊ฐ€๋ณด๋‹ค ์ž‘์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - INVALID_MENU_DISCOUNT_TIME("MEN004", "ํ• ์ธ ์‹œ๊ฐ„์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), - INVALID_MENU_LENGTH("MEN005", "๋ฉ”๋‰ด๋ช… ๊ธธ์ด๊ฐ€ ์ตœ๋Œ€๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค."), + // Cheer + INVALID_CHEER_DESCRIPTION("CHE001", "์‘์› ๋ฉ”์‹œ์ง€๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."), + INVALID_CHEER_IMAGE_KEY("CHE002", "์‘์› ์ด๋ฏธ์ง€ ํ‚ค๊ฐ€ ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.INTERNAL_SERVER_ERROR), - // Bookmark - DUPLICATE_BOOKMARK("BOK001", "์ด๋ฏธ ๋ถ๋งˆํฌ๋œ ๋งค์žฅ์ž…๋‹ˆ๋‹ค."), - BOOKMARK_NOT_FOUND("BOK002", "๋ถ๋งˆํฌ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), - BOOKMARK_MEMBER_REQUIRED("BOK003", "๋ถ๋งˆํฌ ์ƒ์„ฑ ์‹œ ํšŒ์› ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."), - BOOKMARK_STORE_REQUIRED("BOK004", "๋ถ๋งˆํฌ ์ƒ์„ฑ ์‹œ ๊ฐ€๊ฒŒ ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."), + // Map + MAP_SERVER_ERROR("MAP001", "์ง€๋„ ์„œ๋ฒ„์™€์˜ ํ†ต์‹  ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค.", HttpStatus.INTERNAL_SERVER_ERROR), // Auth UNAUTHORIZED_MEMBER("AUTH001", "์ธ์ฆ๋˜์ง€ ์•Š์€ ํšŒ์›์ž…๋‹ˆ๋‹ค.", HttpStatus.UNAUTHORIZED), EXPIRED_TOKEN("AUTH002", "์ด๋ฏธ ๋งŒ๋ฃŒ๋œ ํ† ํฐ์ž…๋‹ˆ๋‹ค.", HttpStatus.UNAUTHORIZED), UNAUTHORIZED_ORIGIN("AUTH003", "ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ์˜ค๋ฆฌ์ง„์ž…๋‹ˆ๋‹ค."), OAUTH_SERVER_ERROR("AUTH003", "OAuth ์„œ๋ฒ„์™€์˜ ํ†ต์‹  ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค.", HttpStatus.INTERNAL_SERVER_ERROR), - ; + + // image + INVALID_IMAGE_TYPE("CLIENT010", "์ง€์›ํ•˜์ง€ ์•Š๋Š” ์ด๋ฏธ์ง€ ํ˜•์‹์ž…๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + FILE_UPLOAD_FAILED("SERVER002", "ํŒŒ์ผ ์—…๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.INTERNAL_SERVER_ERROR), + FILE_URL_GENERATION_FAILED("SERVER003", "ํŒŒ์ผ URL ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.INTERNAL_SERVER_ERROR), + PRESIGNED_URL_GENERATION_FAILED("SERVER004", "Presigned URL ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.INTERNAL_SERVER_ERROR), + + //story + INVALID_STORY_DESCRIPTION("STY001", "์Šคํ† ๋ฆฌ ๋ณธ๋ฌธ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."), + INVALID_STORY_IMAGE_KEY("STY002", "์Šคํ† ๋ฆฌ ์ด๋ฏธ์ง€ Key๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."), + STORY_MEMBER_REQUIRED("STY003", "์Šคํ† ๋ฆฌ ์ž‘์„ฑ ์‹œ ํšŒ์› ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."), + STORY_STORE_REQUIRED("STY004", "์Šคํ† ๋ฆฌ ์ž‘์„ฑ ์‹œ ๊ฐ€๊ฒŒ ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."), + STORY_NOT_FOUND("STY005", "์Šคํ† ๋ฆฌ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + INVALID_STORE_ID("STY006", "์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฐ€๊ฒŒ ID์ž…๋‹ˆ๋‹ค."), + INVALID_STORE_KAKAO_ID("STY007", "์Šคํ† ์–ด Kakao ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."), + INVALID_STORE_NAME("STY008", "์Šคํ† ์–ด ์ด๋ฆ„์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."), + INVALID_STORE_ADDRESS("STY009", "์Šคํ† ์–ด ์ฃผ์†Œ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); private final String code; private final String message; diff --git a/src/main/java/timeeat/exception/BusinessException.java b/src/main/java/eatda/exception/BusinessException.java similarity index 93% rename from src/main/java/timeeat/exception/BusinessException.java rename to src/main/java/eatda/exception/BusinessException.java index bc332e04..8ecceec5 100644 --- a/src/main/java/timeeat/exception/BusinessException.java +++ b/src/main/java/eatda/exception/BusinessException.java @@ -1,4 +1,4 @@ -package timeeat.exception; +package eatda.exception; import lombok.Getter; import org.springframework.http.HttpStatus; diff --git a/src/main/java/timeeat/exception/ErrorResponse.java b/src/main/java/eatda/exception/ErrorResponse.java similarity index 91% rename from src/main/java/timeeat/exception/ErrorResponse.java rename to src/main/java/eatda/exception/ErrorResponse.java index 5a168651..1170cce5 100644 --- a/src/main/java/timeeat/exception/ErrorResponse.java +++ b/src/main/java/eatda/exception/ErrorResponse.java @@ -1,4 +1,4 @@ -package timeeat.exception; +package eatda.exception; public record ErrorResponse(String errorCode, String message) { diff --git a/src/main/java/timeeat/exception/EtcErrorCode.java b/src/main/java/eatda/exception/EtcErrorCode.java similarity index 91% rename from src/main/java/timeeat/exception/EtcErrorCode.java rename to src/main/java/eatda/exception/EtcErrorCode.java index f8e45ef0..94b4a418 100644 --- a/src/main/java/timeeat/exception/EtcErrorCode.java +++ b/src/main/java/eatda/exception/EtcErrorCode.java @@ -1,4 +1,4 @@ -package timeeat.exception; +package eatda.exception; import lombok.Getter; import org.springframework.http.HttpStatus; @@ -15,6 +15,7 @@ public enum EtcErrorCode { NO_COOKIE_FOUND("CLIENT007", "ํ•„์ˆ˜ ์ฟ ํ‚ค ๊ฐ’์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), NO_HEADER_FOUND("CLIENT008", "ํ•„์ˆ˜ ํ—ค๋” ๊ฐ’์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), NO_PARAMETER_FOUND("CLIENT009", "ํ•„์ˆ˜ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ’์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + VALIDATION_ERROR("CLIENT010", "์š”์ฒญ ๋ฐ์ดํ„ฐ ๊ฐ’์ด ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค", HttpStatus.BAD_REQUEST), INTERNAL_SERVER_ERROR("SERVER001", "์„œ๋ฒ„ ๋‚ด๋ถ€ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.INTERNAL_SERVER_ERROR), ; diff --git a/src/main/java/timeeat/exception/GlobalExceptionHandler.java b/src/main/java/eatda/exception/GlobalExceptionHandler.java similarity index 87% rename from src/main/java/timeeat/exception/GlobalExceptionHandler.java rename to src/main/java/eatda/exception/GlobalExceptionHandler.java index 87a86713..c1688b63 100644 --- a/src/main/java/timeeat/exception/GlobalExceptionHandler.java +++ b/src/main/java/eatda/exception/GlobalExceptionHandler.java @@ -1,7 +1,8 @@ -package timeeat.exception; +package eatda.exception; import jakarta.validation.ConstraintViolationException; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.catalina.connector.ClientAbortException; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindException; @@ -12,9 +13,11 @@ import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.HandlerMethodValidationException; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.servlet.resource.NoResourceFoundException; +@Slf4j @RestControllerAdvice @RequiredArgsConstructor public class GlobalExceptionHandler { @@ -75,8 +78,15 @@ public ResponseEntity handleMissingServletRequestParameterExcepti return toErrorResponse(EtcErrorCode.NO_PARAMETER_FOUND); } + @ExceptionHandler(HandlerMethodValidationException.class) + public ResponseEntity handleHandlerMethodValidationException( + HandlerMethodValidationException exception) { + return toErrorResponse(EtcErrorCode.VALIDATION_ERROR); + } + @ExceptionHandler(BusinessException.class) public ResponseEntity handleBusinessException(BusinessException exception) { + log.error("[BusinessException] handled: {}", exception.getErrorCode()); ErrorResponse response = new ErrorResponse(exception.getErrorCode()); return ResponseEntity.status(exception.getStatus()) .body(response); @@ -84,6 +94,7 @@ public ResponseEntity handleBusinessException(BusinessException e @ExceptionHandler(Exception.class) public ResponseEntity handleException(Exception exception) { + log.error("[Unhandled Exception] {}: {}", exception.getClass().getSimpleName(), exception.getMessage(), exception); return toErrorResponse(EtcErrorCode.INTERNAL_SERVER_ERROR); } diff --git a/src/main/java/timeeat/exception/InitializeException.java b/src/main/java/eatda/exception/InitializeException.java similarity index 90% rename from src/main/java/timeeat/exception/InitializeException.java rename to src/main/java/eatda/exception/InitializeException.java index 89bb8470..6a9a5c95 100644 --- a/src/main/java/timeeat/exception/InitializeException.java +++ b/src/main/java/eatda/exception/InitializeException.java @@ -1,4 +1,4 @@ -package timeeat.exception; +package eatda.exception; public class InitializeException extends RuntimeException { diff --git a/src/main/java/timeeat/repository/member/MemberRepository.java b/src/main/java/eatda/repository/member/MemberRepository.java similarity index 77% rename from src/main/java/timeeat/repository/member/MemberRepository.java rename to src/main/java/eatda/repository/member/MemberRepository.java index 9e25fc02..b74afa35 100644 --- a/src/main/java/timeeat/repository/member/MemberRepository.java +++ b/src/main/java/eatda/repository/member/MemberRepository.java @@ -1,10 +1,10 @@ -package timeeat.repository.member; +package eatda.repository.member; +import eatda.domain.member.Member; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; import java.util.Optional; import org.springframework.data.repository.Repository; -import timeeat.domain.member.Member; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; public interface MemberRepository extends Repository { diff --git a/src/main/java/eatda/repository/store/CheerRepository.java b/src/main/java/eatda/repository/store/CheerRepository.java new file mode 100644 index 00000000..0ea9c490 --- /dev/null +++ b/src/main/java/eatda/repository/store/CheerRepository.java @@ -0,0 +1,23 @@ +package eatda.repository.store; + +import eatda.domain.store.Cheer; +import eatda.domain.store.Store; +import java.util.List; +import java.util.Optional; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; + +public interface CheerRepository extends Repository { + + Cheer save(Cheer cheer); + + List findAllByOrderByCreatedAtDesc(Pageable pageable); + + @Query(""" + SELECT c.imageKey FROM Cheer c + WHERE c.store = :store AND c.imageKey IS NOT NULL + ORDER BY c.createdAt DESC + LIMIT 1""") + Optional findRecentImageKey(Store store); +} diff --git a/src/main/java/eatda/repository/store/StoreRepository.java b/src/main/java/eatda/repository/store/StoreRepository.java new file mode 100644 index 00000000..8fc482fb --- /dev/null +++ b/src/main/java/eatda/repository/store/StoreRepository.java @@ -0,0 +1,13 @@ +package eatda.repository.store; + +import eatda.domain.store.Store; +import java.util.List; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.Repository; + +public interface StoreRepository extends Repository { + + Store save(Store store); + + List findAllByOrderByCreatedAtDesc(Pageable pageable); +} diff --git a/src/main/java/eatda/repository/story/StoryRepository.java b/src/main/java/eatda/repository/story/StoryRepository.java new file mode 100644 index 00000000..f17c9e12 --- /dev/null +++ b/src/main/java/eatda/repository/story/StoryRepository.java @@ -0,0 +1,19 @@ +package eatda.repository.story; + +import eatda.domain.story.Story; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; +import java.util.Optional; +import org.springframework.data.repository.Repository; + +public interface StoryRepository extends Repository { + + Story save(Story story); + + Optional findById(Long id); + + default Story getById(Long id) { + return findById(id) + .orElseThrow(() -> new BusinessException(BusinessErrorCode.STORY_NOT_FOUND)); + } +} diff --git a/src/main/java/timeeat/service/auth/AuthService.java b/src/main/java/eatda/service/auth/AuthService.java similarity index 76% rename from src/main/java/timeeat/service/auth/AuthService.java rename to src/main/java/eatda/service/auth/AuthService.java index 24032661..a71d5dc3 100644 --- a/src/main/java/timeeat/service/auth/AuthService.java +++ b/src/main/java/eatda/service/auth/AuthService.java @@ -1,18 +1,18 @@ -package timeeat.service.auth; +package eatda.service.auth; +import eatda.client.oauth.OauthClient; +import eatda.client.oauth.OauthMemberInformation; +import eatda.client.oauth.OauthToken; +import eatda.controller.auth.LoginRequest; +import eatda.controller.member.MemberResponse; +import eatda.domain.member.Member; +import eatda.repository.member.MemberRepository; import java.net.URI; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import timeeat.client.oauth.OauthClient; -import timeeat.client.oauth.OauthMemberInformation; -import timeeat.client.oauth.OauthToken; -import timeeat.controller.auth.LoginRequest; -import timeeat.controller.member.MemberResponse; -import timeeat.domain.member.Member; -import timeeat.repository.member.MemberRepository; @Slf4j @Service diff --git a/src/main/java/eatda/service/common/ImageDomain.java b/src/main/java/eatda/service/common/ImageDomain.java new file mode 100644 index 00000000..5d155c0b --- /dev/null +++ b/src/main/java/eatda/service/common/ImageDomain.java @@ -0,0 +1,15 @@ +package eatda.service.common; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ImageDomain { + ARTICLE("article"), + STORE("store"), + MEMBER("member"), + STORY("story"); + + private final String name; +} diff --git a/src/main/java/eatda/service/common/ImageService.java b/src/main/java/eatda/service/common/ImageService.java new file mode 100644 index 00000000..676a89d4 --- /dev/null +++ b/src/main/java/eatda/service/common/ImageService.java @@ -0,0 +1,100 @@ +package eatda.service.common; + +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; +import java.io.IOException; +import java.time.Duration; +import java.util.Set; +import java.util.UUID; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; + +@Service +public class ImageService { + + private static final Set ALLOWED_CONTENT_TYPES = Set.of("image/jpg", "image/jpeg", "image/png"); + private static final String DEFAULT_CONTENT_TYPE = "bin"; + private static final String PATH_DELIMITER = "/"; + private static final String EXTENSION_DELIMITER = "."; + private static final Duration PRESIGNED_URL_DURATION = Duration.ofMinutes(30); + + private final S3Client s3Client; + private final String bucket; + private final S3Presigner s3Presigner; + + public ImageService( + S3Client s3Client, + @Value("${spring.cloud.aws.s3.bucket}") String bucket, + S3Presigner s3Presigner) { + this.s3Client = s3Client; + this.bucket = bucket; + this.s3Presigner = s3Presigner; + } + + public String upload(MultipartFile file, ImageDomain domain) { + validateContentType(file); + String extension = getExtension(file.getOriginalFilename()); + String uuid = UUID.randomUUID().toString(); + String key = domain.getName() + PATH_DELIMITER + uuid + EXTENSION_DELIMITER + extension; + + try { + PutObjectRequest request = PutObjectRequest.builder() + .bucket(bucket) + .key(key) + .contentType(file.getContentType()) + .build(); + + s3Client.putObject(request, RequestBody.fromInputStream(file.getInputStream(), file.getSize())); + return key; + } catch (IOException exception) { + throw new BusinessException(BusinessErrorCode.FILE_UPLOAD_FAILED); + } + } + + private void validateContentType(MultipartFile file) { + String contentType = file.getContentType(); + if (!ALLOWED_CONTENT_TYPES.contains(contentType)) { + throw new BusinessException(BusinessErrorCode.INVALID_IMAGE_TYPE); + } + } + + private String getExtension(String filename) { + if (filename == null + || filename.lastIndexOf(EXTENSION_DELIMITER) == -1 + || filename.startsWith(EXTENSION_DELIMITER)) { + return DEFAULT_CONTENT_TYPE; + } + return filename.substring(filename.lastIndexOf(EXTENSION_DELIMITER) + 1); + } + + @Nullable + public String getPresignedUrl(@Nullable String key) { + if (key == null) { + return null; + } + + try { + GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(bucket) + .key(key) + .build(); + + GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder() + .getObjectRequest(getObjectRequest) + .signatureDuration(PRESIGNED_URL_DURATION) + .build(); + + return s3Presigner.presignGetObject(presignRequest).url().toString(); + } catch (Exception exception) { + throw new BusinessException(BusinessErrorCode.PRESIGNED_URL_GENERATION_FAILED); + } + } +} diff --git a/src/main/java/timeeat/service/service/MemberService.java b/src/main/java/eatda/service/member/MemberService.java similarity index 78% rename from src/main/java/timeeat/service/service/MemberService.java rename to src/main/java/eatda/service/member/MemberService.java index 95735437..37f33555 100644 --- a/src/main/java/timeeat/service/service/MemberService.java +++ b/src/main/java/eatda/service/member/MemberService.java @@ -1,14 +1,14 @@ -package timeeat.service.service; - +package eatda.service.member; + +import eatda.controller.member.MemberResponse; +import eatda.controller.member.MemberUpdateRequest; +import eatda.domain.member.Member; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; +import eatda.repository.member.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import timeeat.controller.member.MemberResponse; -import timeeat.controller.member.MemberUpdateRequest; -import timeeat.domain.member.Member; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; -import timeeat.repository.member.MemberRepository; @Service @RequiredArgsConstructor @@ -16,6 +16,12 @@ public class MemberService { private final MemberRepository memberRepository; + @Transactional(readOnly = true) + public MemberResponse getMember(long memberId) { + Member member = memberRepository.getById(memberId); + return new MemberResponse(member); + } + @Transactional(readOnly = true) public void validateNickname(String nickname, long memberId) { Member member = memberRepository.getById(memberId); diff --git a/src/main/java/eatda/service/store/CheerService.java b/src/main/java/eatda/service/store/CheerService.java new file mode 100644 index 00000000..e8a98a39 --- /dev/null +++ b/src/main/java/eatda/service/store/CheerService.java @@ -0,0 +1,33 @@ +package eatda.service.store; + +import eatda.controller.store.CheerPreviewResponse; +import eatda.controller.store.CheersResponse; +import eatda.domain.store.Cheer; +import eatda.repository.store.CheerRepository; +import eatda.service.common.ImageService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class CheerService { + + private final ImageService imageService; + private final CheerRepository cheerRepository; + + @Transactional(readOnly = true) + public CheersResponse getCheers(int size) { + List cheers = cheerRepository.findAllByOrderByCreatedAtDesc(Pageable.ofSize(size)); + return toCheersResponse(cheers); + } + + private CheersResponse toCheersResponse(List cheers) { + return new CheersResponse(cheers.stream() + .map(cheer -> new CheerPreviewResponse(cheer, cheer.getStore(), + imageService.getPresignedUrl(cheer.getImageKey()))) + .toList()); + } +} diff --git a/src/main/java/eatda/service/store/StoreSearchFilter.java b/src/main/java/eatda/service/store/StoreSearchFilter.java new file mode 100644 index 00000000..0604c68d --- /dev/null +++ b/src/main/java/eatda/service/store/StoreSearchFilter.java @@ -0,0 +1,19 @@ +package eatda.service.store; + +import eatda.client.map.StoreSearchResult; +import java.util.List; +import org.springframework.stereotype.Component; + +@Component +public class StoreSearchFilter { + + public List filterSearchedStores(List searchResults) { + return searchResults.stream() + .filter(this::isValidStore) + .toList(); + } + + private boolean isValidStore(StoreSearchResult store) { + return store.isFoodStore() && store.isInSeoul(); + } +} diff --git a/src/main/java/eatda/service/store/StoreService.java b/src/main/java/eatda/service/store/StoreService.java new file mode 100644 index 00000000..ded2b722 --- /dev/null +++ b/src/main/java/eatda/service/store/StoreService.java @@ -0,0 +1,54 @@ +package eatda.service.store; + +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toList; + +import eatda.client.map.MapClient; +import eatda.client.map.StoreSearchResult; +import eatda.controller.store.StorePreviewResponse; +import eatda.controller.store.StoreSearchResponses; +import eatda.controller.store.StoresResponse; +import eatda.domain.store.Store; +import eatda.repository.store.CheerRepository; +import eatda.repository.store.StoreRepository; +import eatda.service.common.ImageService; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class StoreService { + + private final MapClient mapClient; + private final StoreSearchFilter storeSearchFilter; + private final StoreRepository storeRepository; + private final CheerRepository cheerRepository; + private final ImageService imageService; + + // TODO : N+1 ๋ฌธ์ œ ํ•ด๊ฒฐ + public StoresResponse getStores(int size) { + return storeRepository.findAllByOrderByCreatedAtDesc(Pageable.ofSize(size)) + .stream() + .map(store -> new StorePreviewResponse(store, getStoreImageUrl(store).orElse(null))) + .collect(collectingAndThen(toList(), StoresResponse::new)); + } + + private Optional getStoreImageUrl(Store store) { + return cheerRepository.findRecentImageKey(store) + .map(imageService::getPresignedUrl); + } + + public StoreSearchResponses searchStores(String query) { + List searchResults = mapClient.searchShops(query); + List filteredResults = storeSearchFilter.filterSearchedStores(searchResults); + return StoreSearchResponses.from(filteredResults); + } + + public List searchStoreResults(String query) { + List searchResults = mapClient.searchShops(query); + return storeSearchFilter.filterSearchedStores(searchResults); + } +} diff --git a/src/main/java/eatda/service/story/StoryService.java b/src/main/java/eatda/service/story/StoryService.java new file mode 100644 index 00000000..7724b1cb --- /dev/null +++ b/src/main/java/eatda/service/story/StoryService.java @@ -0,0 +1,62 @@ +package eatda.service.story; + +import eatda.client.map.StoreSearchResult; +import eatda.controller.story.FilteredSearchResult; +import eatda.controller.story.StoryRegisterRequest; +import eatda.domain.member.Member; +import eatda.domain.story.Story; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; +import eatda.repository.member.MemberRepository; +import eatda.repository.story.StoryRepository; +import eatda.service.common.ImageDomain; +import eatda.service.common.ImageService; +import eatda.service.store.StoreService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +@Service +@RequiredArgsConstructor +public class StoryService { + + private final StoreService storeService; + private final ImageService imageService; + private final StoryRepository storyRepository; + private final MemberRepository memberRepository; + + @Transactional + public void registerStory(StoryRegisterRequest request, MultipartFile image, Long memberId) { + Member member = memberRepository.getById(memberId); + List searchResponses = storeService.searchStoreResults(request.query()); + FilteredSearchResult matchedStore = filteredSearchResponse(searchResponses, request.storeKakaoId()); + String imageKey = imageService.upload(image, ImageDomain.STORY); + + Story story = Story.builder() + .member(member) + .storeKakaoId(matchedStore.kakaoId()) + .storeName(matchedStore.name()) + .storeAddress(matchedStore.address()) + .storeCategory(matchedStore.category()) + .description(request.description()) + .imageKey(imageKey) + .build(); + + storyRepository.save(story); + } + + private FilteredSearchResult filteredSearchResponse(List responses, String storeKakaoId) { + return responses.stream() + .filter(store -> store.kakaoId().equals(storeKakaoId)) + .findFirst() + .map(store -> new FilteredSearchResult( + store.kakaoId(), + store.name(), + store.roadAddress(), + store.categoryName() + )) + .orElseThrow(() -> new BusinessException(BusinessErrorCode.STORE_NOT_FOUND)); + } +} diff --git a/src/main/java/timeeat/domain/bookmark/Bookmark.java b/src/main/java/timeeat/domain/bookmark/Bookmark.java deleted file mode 100644 index 2ca6a982..00000000 --- a/src/main/java/timeeat/domain/bookmark/Bookmark.java +++ /dev/null @@ -1,49 +0,0 @@ -package timeeat.domain.bookmark; - -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import timeeat.domain.member.Member; -import timeeat.domain.store.Store; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; - -@Table(name = "bookmark") -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Bookmark { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false) - private Member member; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "store_id", nullable = false) - private Store store; - - public Bookmark(Member member, Store store) { - validateMember(member); - validateStore(store); - this.member = member; - this.store = store; - } - - private void validateMember(Member member) { - if (member == null) { - throw new BusinessException(BusinessErrorCode.BOOKMARK_MEMBER_REQUIRED); - } - } - - private void validateStore(Store store) { - if (store == null) { - throw new BusinessException(BusinessErrorCode.BOOKMARK_STORE_REQUIRED); - } - } -} - diff --git a/src/main/java/timeeat/domain/menu/Discount.java b/src/main/java/timeeat/domain/menu/Discount.java deleted file mode 100644 index 48983f52..00000000 --- a/src/main/java/timeeat/domain/menu/Discount.java +++ /dev/null @@ -1,45 +0,0 @@ -package timeeat.domain.menu; - -import java.time.LocalDateTime; -import jakarta.persistence.Embeddable; -import lombok.AccessLevel; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; - -@Getter -@Embeddable -@EqualsAndHashCode -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Discount { - private static final int MIN_PRICE = 1; - - private Integer discountPrice; - private LocalDateTime startTime; - private LocalDateTime endTime; - - public Discount(Price originalPrice, Integer discountPrice, LocalDateTime startTime, LocalDateTime endTime) { - validatePrice(originalPrice, discountPrice); - validateTime(startTime, endTime); - this.discountPrice = discountPrice; - this.startTime = startTime; - this.endTime = endTime; - } - - private void validatePrice(Price originalPrice, Integer discountPrice) { - if (discountPrice != null) { - if (discountPrice < MIN_PRICE || discountPrice >= originalPrice.getValue()) { - throw new BusinessException(BusinessErrorCode.INVALID_MENU_DISCOUNT_PRICE); - } - } - } - - private void validateTime(LocalDateTime startTime, LocalDateTime endTime) { - if (startTime != null && endTime != null && startTime.isAfter(endTime)) { - throw new BusinessException(BusinessErrorCode.INVALID_MENU_DISCOUNT_TIME); - } - } -} - diff --git a/src/main/java/timeeat/domain/menu/Menu.java b/src/main/java/timeeat/domain/menu/Menu.java deleted file mode 100644 index 36c3dc47..00000000 --- a/src/main/java/timeeat/domain/menu/Menu.java +++ /dev/null @@ -1,68 +0,0 @@ -package timeeat.domain.menu; - -import java.time.LocalDateTime; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; - -@Table(name = "menu") -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Menu { - - private static final int MAX_NAME_LENGTH = 255; - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "name", nullable = false) - private String name; - - @Column(name = "description") - private String description; - - @Column(name = "image_url") - private String imageUrl; - - @Embedded - private Price price; - - @Embedded - private Discount discount; - - public Menu( - String name, - String description, - Integer price, - String imageUrl, - Integer discountPrice, - LocalDateTime discountStartTime, - LocalDateTime discountEndTime - ) { - validateName(name); - validateNameLength(name); - - this.name = name; - this.description = description; - this.imageUrl = imageUrl; - this.price = new Price(price); - this.discount = new Discount(this.price, discountPrice, discountStartTime, discountEndTime); - } - - private void validateName(String name) { - if (name == null || name.trim().isEmpty()) { - throw new BusinessException(BusinessErrorCode.INVALID_MENU_NAME); - } - } - - private void validateNameLength(String name) { - if (name.length() > MAX_NAME_LENGTH) { - throw new BusinessException(BusinessErrorCode.INVALID_MENU_LENGTH); - } - } -} diff --git a/src/main/java/timeeat/domain/menu/Price.java b/src/main/java/timeeat/domain/menu/Price.java deleted file mode 100644 index 8f2afe95..00000000 --- a/src/main/java/timeeat/domain/menu/Price.java +++ /dev/null @@ -1,33 +0,0 @@ -package timeeat.domain.menu; - -import jakarta.persistence.Column; -import jakarta.persistence.Embeddable; -import lombok.AccessLevel; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; - -@Getter -@Embeddable -@EqualsAndHashCode -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Price { - - private static final int MIN_PRICE = 1; - - @Column(name = "price", nullable = false) - private Integer value; - - public Price(Integer value) { - validateMinPrice(value); - this.value = value; - } - - private void validateMinPrice(Integer value) { - if (value == null || value < MIN_PRICE) { - throw new BusinessException(BusinessErrorCode.INVALID_MENU_PRICE); - } - } -} diff --git a/src/main/java/timeeat/domain/store/Store.java b/src/main/java/timeeat/domain/store/Store.java deleted file mode 100644 index 56df106b..00000000 --- a/src/main/java/timeeat/domain/store/Store.java +++ /dev/null @@ -1,94 +0,0 @@ -package timeeat.domain.store; - -import java.time.LocalTime; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import timeeat.enums.InterestArea; -import timeeat.enums.StoreCategory; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; - -@Table(name = "store") -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Store { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "name", nullable = false) - private String name; - - @Enumerated(EnumType.STRING) - @Column(name = "category", nullable = false) - private StoreCategory category; - - @Embedded - private Coordinates coordinates; - - @Column(name = "address", nullable = false) - private String address; - - @Embedded - private StorePhoneNumber storePhoneNumber; - - @Column(name = "image_url") - private String imageUrl; - - @Embedded - private StoreHours storeHours; - - @Column(name = "introduction") - private String introduction; - - @Enumerated(EnumType.STRING) - @Column(name = "interest_area", nullable = false) - private InterestArea interestArea; - - //TODO ๋นŒ๋” ํŒจํ„ด์œผ๋กœ ๋ณ€๊ฒฝ ํ•ฉ์˜, ๋‹ค์Œ ๋„๋ฉ”์ธ ์ˆ˜์ •์‹œ ๋ฐ˜์˜ ํ•„์š” - - public Store( - String name, - String category, - String address, - Double latitude, - Double longitude, - String phoneNumber, - String imageUrl, - LocalTime openTime, - LocalTime closeTime, - String introduction, - String interestArea - ) { - - validateName(name); - validateAddress(address); - - this.name = name; - this.address = address; - this.imageUrl = imageUrl; - this.introduction = introduction; - - this.category = StoreCategory.from(category); - this.interestArea = InterestArea.from(interestArea); - this.coordinates = new Coordinates(latitude, longitude); - this.storePhoneNumber = new StorePhoneNumber(phoneNumber); - this.storeHours = new StoreHours(openTime, closeTime); - } - - private void validateName(String name) { - if (name == null || name.trim().isEmpty()) { - throw new BusinessException(BusinessErrorCode.INVALID_STORE_NAME); - } - } - - private void validateAddress(String address) { - if (address == null || address.trim().isEmpty()) { - throw new BusinessException(BusinessErrorCode.INVALID_STORE_ADDRESS); - } - } -} diff --git a/src/main/java/timeeat/domain/store/StoreHours.java b/src/main/java/timeeat/domain/store/StoreHours.java deleted file mode 100644 index 2cb439d4..00000000 --- a/src/main/java/timeeat/domain/store/StoreHours.java +++ /dev/null @@ -1,43 +0,0 @@ -package timeeat.domain.store; - -import java.time.LocalTime; -import jakarta.persistence.Embeddable; -import lombok.AccessLevel; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; - -@Getter -@Embeddable -@EqualsAndHashCode -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class StoreHours { - - private LocalTime openTime; - private LocalTime closeTime; - - public StoreHours(LocalTime openTime, LocalTime closeTime) { - validate(openTime, closeTime); - this.openTime = openTime; - this.closeTime = closeTime; - } - - private void validate(LocalTime openTime, LocalTime closeTime) { - validateNotNull(openTime, closeTime); - validateOrder(openTime, closeTime); - } - - private void validateNotNull(LocalTime openTime, LocalTime closeTime) { - if (openTime == null || closeTime == null) { - throw new BusinessException(BusinessErrorCode.INVALID_STORE_TIME_NULL); - } - } - - private void validateOrder(LocalTime openTime, LocalTime closeTime) { - if (openTime.isAfter(closeTime)) { - throw new BusinessException(BusinessErrorCode.INVALID_STORE_TIME_ORDER); - } - } -} diff --git a/src/main/java/timeeat/domain/store/StorePhoneNumber.java b/src/main/java/timeeat/domain/store/StorePhoneNumber.java deleted file mode 100644 index c3b84b5d..00000000 --- a/src/main/java/timeeat/domain/store/StorePhoneNumber.java +++ /dev/null @@ -1,34 +0,0 @@ -package timeeat.domain.store; - -import java.util.regex.Pattern; -import jakarta.persistence.Column; -import jakarta.persistence.Embeddable; -import lombok.AccessLevel; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; - -@Getter -@Embeddable -@EqualsAndHashCode -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class StorePhoneNumber { - - private static final Pattern PHONE_NUMBER_PATTERN = Pattern.compile("^\\d{8,12}$"); - - @Column(name = "phone_number", nullable = false) - private String value; - - public StorePhoneNumber(String value) { - validateNumber(value); - this.value = value; - } - - private void validateNumber(String number) { - if (number != null && !PHONE_NUMBER_PATTERN.matcher(number).matches()) { - throw new BusinessException(BusinessErrorCode.INVALID_STORE_PHONE_NUMBER); - } - } -} diff --git a/src/main/java/timeeat/enums/InterestArea.java b/src/main/java/timeeat/enums/InterestArea.java deleted file mode 100644 index 75c334a2..00000000 --- a/src/main/java/timeeat/enums/InterestArea.java +++ /dev/null @@ -1,62 +0,0 @@ -package timeeat.enums; - -import java.util.Arrays; -import lombok.Getter; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; - -@Getter -public enum InterestArea { - - JONGNO("์ข…๋กœ๊ตฌ"), - JUNG("์ค‘๊ตฌ"), - YONGSAN("์šฉ์‚ฐ๊ตฌ"), - SEONGDONG("์„ฑ๋™๊ตฌ"), - GWANGJIN("๊ด‘์ง„๊ตฌ"), - DONGDAEMUN("๋™๋Œ€๋ฌธ๊ตฌ"), - JUNGRANG("์ค‘๋ž‘๊ตฌ"), - SEONGBUK("์„ฑ๋ถ๊ตฌ"), - GANGBUK("๊ฐ•๋ถ๊ตฌ"), - DOBONG("๋„๋ด‰๊ตฌ"), - NOWON("๋…ธ์›๊ตฌ"), - EUNPYEONG("์€ํ‰๊ตฌ"), - SEODAEMUN("์„œ๋Œ€๋ฌธ๊ตฌ"), - MAPO("๋งˆํฌ๊ตฌ"), - YANGCHEON("์–‘์ฒœ๊ตฌ"), - GANGSEO("๊ฐ•์„œ๊ตฌ"), - GURO("๊ตฌ๋กœ๊ตฌ"), - GEUMCHEON("๊ธˆ์ฒœ๊ตฌ"), - YEONGDEUNGPO("์˜๋“ฑํฌ๊ตฌ"), - DONGJAK("๋™์ž‘๊ตฌ"), - GWANAK("๊ด€์•…๊ตฌ"), - SEOCHO("์„œ์ดˆ๊ตฌ"), - GANGNAM("๊ฐ•๋‚จ๊ตฌ"), - SONGPA("์†กํŒŒ๊ตฌ"), - GANGDONG("๊ฐ•๋™๊ตฌ"); - - private final String areaName; - - InterestArea(String areaName) { - this.areaName = areaName; - } - - public static InterestArea from(String value) { - if (value == null || value.trim().isEmpty()) { - throw new BusinessException(BusinessErrorCode.INVALID_INTEREST_AREA); - } - - return Arrays.stream(values()) - .filter(area -> area.areaName.equals(value)) - .findFirst() - .orElseThrow(() -> new BusinessException(BusinessErrorCode.INVALID_INTEREST_AREA)); - } - - public static boolean isValid(String value) { - try { - from(value); - return true; - } catch (BusinessException e) { - return false; - } - } -} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index f64d06f6..cfb28b12 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -7,6 +7,14 @@ spring: aws: region: static: ap-northeast-2 + s3: + bucket: eatda-storage-dev + + servlet: + multipart: + max-file-size: 5MB + max-request-size: 20MB + config: import: "aws-parameterstore:/dev/" @@ -43,3 +51,6 @@ oauth: allowed-origins: - "http://localhost:3000" - "https://dev.eatda.net" + +kakao: + api-key: ${KAKAO_API_KEY} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 82b90fea..e9b373d4 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -10,6 +10,18 @@ spring: password: ${LOCAL_DB_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver + cloud: + aws: + region: + static: ap-northeast-2 + s3: + bucket: eatda-storage-local + + servlet: + multipart: + max-file-size: 5MB + max-request-size: 20MB + jpa: hibernate: ddl-auto: validate @@ -34,3 +46,6 @@ oauth: redirect-path: /login/callback allowed-origins: - "http://localhost:3000" + +kakao: + api-key: ${KAKAO_API_KEY} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index dffbac3d..e15c334d 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -7,6 +7,14 @@ spring: aws: region: static: ap-northeast-2 + s3: + bucket: eatda-storage-prod + + servlet: + multipart: + max-file-size: 5MB + max-request-size: 10MB + config: import: "aws-parameterstore:/prod/" @@ -43,3 +51,6 @@ oauth: allowed-origins: - "https://www.eatda.net" - "https://eatda.net" + +kakao: + api-key: ${KAKAO_API_KEY} diff --git a/src/main/resources/db/migration/V1__init.sql b/src/main/resources/db/migration/V1__init.sql index 5e9a73ca..79d9e669 100644 --- a/src/main/resources/db/migration/V1__init.sql +++ b/src/main/resources/db/migration/V1__init.sql @@ -1,50 +1,52 @@ -CREATE TABLE `store` +CREATE TABLE `member` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `name` VARCHAR(255) NOT NULL, - `category` VARCHAR(255) NOT NULL, - `introduction` TEXT NOT NULL, - `phone_number` VARCHAR(255) NOT NULL COMMENT '(`-` ์—†์ด))', - `interest_area` VARCHAR(50) NOT NULL COMMENT '(์„œ์šธ์‹œ 25๊ฐœ ๊ตฌ, Java Enum ์ด๋ฆ„์œผ๋กœ ์ €์žฅ: ex, GANGNAM)', - `address` VARCHAR(255) NOT NULL COMMENT '(์ „์ฒด์ฃผ์†Œ)', - `latitude` DOUBLE NOT NULL, - `longitude` DOUBLE NOT NULL, - `open_time` TIME NOT NULL, - `close_time` TIME NOT NULL, - `image_url` VARCHAR(511) NULL, + `id` BIGINT NOT NULL AUTO_INCREMENT, + `email` VARCHAR(255) NOT NULL UNIQUE, + `social_id` VARCHAR(255) NOT NULL UNIQUE, + `nickname` VARCHAR(255) NULL, + `phone_number` VARCHAR(255) NULL, + `opt_in_marketing` BOOLEAN NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ); -CREATE TABLE `member` +CREATE TABLE `store` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `social_id` VARCHAR(255) NOT NULL, - `nickname` VARCHAR(255) NULL, - `phone_number` VARCHAR(255) NULL COMMENT '(`-` ์—†์ด))', - `interest_area` VARCHAR(50) NULL COMMENT '(์„œ์šธ์‹œ 25๊ฐœ ๊ตฌ, Java Enum ์ด๋ฆ„์œผ๋กœ ์ €์žฅ: ex, JONGNO)', - `opt_in_marketing` BOOLEAN NULL DEFAULT true, + `id` BIGINT NOT NULL AUTO_INCREMENT, + `kakao_id` VARCHAR(255) NOT NULL UNIQUE, + `category` VARCHAR(50) NOT NULL, + `phone_number` VARCHAR(255) NOT NULL, + `name` VARCHAR(255) NOT NULL, + `place_url` VARCHAR(255) NOT NULL, + `road_address` VARCHAR(255) NOT NULL, + `lot_number_address` VARCHAR(255) NOT NULL, + `latitude` DOUBLE NOT NULL, + `longitude` DOUBLE NOT NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ); -CREATE TABLE `bookmark` +CREATE TABLE `cheer` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `member_id` BIGINT NOT NULL, - `store_id` BIGINT NOT NULL, + `id` BIGINT NOT NULL AUTO_INCREMENT, + `member_id` BIGINT NOT NULL, + `store_id` BIGINT NOT NULL, + `description` TEXT NOT NULL, + `image_key` VARCHAR(511) NULL, + `is_admin` BOOLEAN NOT NULL DEFAULT FALSE, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), - UNIQUE KEY `UK_member_store` (`member_id`, `store_id`) + FOREIGN KEY (`member_id`) REFERENCES `member` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`store_id`) REFERENCES `store` (`id`) ON DELETE CASCADE ); -CREATE TABLE `menu` +CREATE TABLE `article` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `store_id` BIGINT NOT NULL, - `name` VARCHAR(255) NOT NULL, - `description` VARCHAR(255) NULL, - `price` INTEGER NOT NULL, - `discount_price` INTEGER NULL, - `start_time` DATETIME NULL, - `end_time` DATETIME NULL, - `image_url` VARCHAR(511) NULL, + `id` BIGINT NOT NULL AUTO_INCREMENT, + `title` VARCHAR(255) NOT NULL, + `subtitle` VARCHAR(255) NOT NULL, + `article_url` VARCHAR(511) NOT NULL, + `image_key` VARCHAR(511) NOT NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ); diff --git a/src/main/resources/db/migration/V3__add_story_table.sql b/src/main/resources/db/migration/V3__add_story_table.sql new file mode 100644 index 00000000..32c7e6ad --- /dev/null +++ b/src/main/resources/db/migration/V3__add_story_table.sql @@ -0,0 +1,13 @@ +CREATE TABLE `story` +( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `member_id` BIGINT NOT NULL, + `store_kakao_id` VARCHAR(255) NOT NULL, + `store_name` VARCHAR(255) NOT NULL, + `store_address` VARCHAR(255) NOT NULL, + `store_category` VARCHAR(50) NOT NULL, + `description` TEXT NOT NULL, + `image_key` VARCHAR(511) NOT NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +); diff --git a/src/main/resources/db/seed/dev/V2__dev_init_data.sql b/src/main/resources/db/seed/dev/V2__dev_init_data.sql index 40f48ac2..b9fa13ed 100644 --- a/src/main/resources/db/seed/dev/V2__dev_init_data.sql +++ b/src/main/resources/db/seed/dev/V2__dev_init_data.sql @@ -1,112 +1,39 @@ -INSERT INTO member (id, social_id, nickname, phone_number, interest_area, opt_in_marketing) - VALUES (1, 123456789, '์ด์Šน๋กœ', '01012345678', 'GANGNAM', true), - (2, 987654321, '์ด์ถฉ์•ˆ', '01087654321', 'SEOCHO', false), - (3, 456789123, '์žฅ์ˆ˜๋นˆ', '01045678912', 'SONGPA', true), - (4, 789123456, '์„œ์ค€ํ™˜', '01078912345', 'MAPO', true), - (5, 321251287, '์‹ ๋ฏผ์„ ', '01034574568', 'GANGSEO', false), - (6, 324569987, '๋ฐ•ํฌ์ˆ˜', '01043609998', 'SEODAEMUN', false), - (7, 323487985, 'ํ•˜์•„์–€', '01065083298', 'SEONGDONG', false) AS new -ON DUPLICATE KEY UPDATE social_id = new.social_id, - nickname = new.nickname, - phone_number = new.phone_number, - interest_area = new.interest_area, - opt_in_marketing = new.opt_in_marketing; +INSERT INTO member (id, social_id, email, nickname, phone_number, opt_in_marketing) +VALUES (1, 123456789, 'default1@kakao.com', '์ด์Šน๋กœ', '01012345678', true), + (2, 987654321, 'default2@kakao.com', '์ด์ถฉ์•ˆ', '01087654321', false), + (3, 456789123, 'default3@kakao.com', '์žฅ์ˆ˜๋นˆ', '01045678912', true), + (4, 789123456, 'default4@kakao.com', '์„œ์ค€ํ™˜', '01078912345', true), + (5, 321251287, 'default5@kakao.com', '์‹ ๋ฏผ์„ ', '01034574568', false), + (6, 324569987, 'default6@kakao.com', '๋ฐ•ํฌ์ˆ˜', '01043609998', false), + (7, 323487985, 'default7@kakao.com', 'ํ•˜์•„์–€', '01065083298', false); -INSERT INTO store (id, name, category, latitude, longitude, address, phone_number, image_url, open_time, close_time, - introduction, interest_area) - VALUES (1, '๋ง›์žˆ๋Š” ํ•œ์‹๋‹น', 'ํ•œ์‹', 37.5665, 126.9780, '์„œ์šธํŠน๋ณ„์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123', '0212345678', 'https://example.com/store1.jpg', - '09:00:00', '22:00:00', '์ „ํ†ต ํ•œ์‹์˜ ๋ง›์„ ๋А๋‚„ ์ˆ˜ ์žˆ๋Š” ๊ณณ์ž…๋‹ˆ๋‹ค.', 'GANGNAM'), - (2, '์‹ ์„ ํ•œ ์ค‘์‹๋‹น', '์ค‘์‹', 37.5645, 126.9770, '์„œ์šธํŠน๋ณ„์‹œ ์„œ์ดˆ๊ตฌ ์„œ์ดˆ๋Œ€๋กœ 456', '0287654321', 'https://example.com/store2.jpg', - '11:00:00', '21:00:00', '์ •ํ†ต ์ค‘๊ตญ ์š”๋ฆฌ๋ฅผ ๋ง›๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.', 'SEOCHO'), - (3, '์ผ๋ณธ ์ •ํ†ต ์Šค์‹œ', '์ผ์‹', 37.5685, 126.9790, '์„œ์šธํŠน๋ณ„์‹œ ์†กํŒŒ๊ตฌ ์˜ฌ๋ฆผํ”ฝ๋กœ 789', '0245678912', - 'https://example.com/store3.jpg', - '11:30:00', '22:30:00', '์‹ ์„ ํ•œ ํšŒ์™€ ์ •ํ†ต ์Šค์‹œ๋ฅผ ์ฆ๊ธฐ์„ธ์š”.', 'SONGPA'), - (4, '์ดํƒˆ๋ฆฌ์•„ ํŒŒ์Šคํƒ€', '์–‘์‹', 37.5625, 126.9760, '์„œ์šธํŠน๋ณ„์‹œ ๋งˆํฌ๊ตฌ ํ™๋Œ€๋กœ 321', '0278912345', 'https://example.com/store4.jpg', - '12:00:00', '23:00:00', '์ •ํ†ต ์ดํƒˆ๋ฆฌ์•„ ํŒŒ์Šคํƒ€์™€ ํ”ผ์ž๋ฅผ ๋งŒ๋‚˜๋ณด์„ธ์š”.', 'MAPO'), - (5, '์ปคํ”ผํ–ฅ ๊ฐ€๋“ํ•œ ์นดํŽ˜', '์นดํŽ˜', 37.5705, 126.9800, '์„œ์šธํŠน๋ณ„์‹œ ์ข…๋กœ๊ตฌ ์ข…๋กœ 654', '0232165498', - 'https://example.com/store5.jpg', - '07:00:00', '23:59:59', '์•„๋Š‘ํ•œ ๋ถ„์œ„๊ธฐ์—์„œ ์ปคํ”ผ๋ฅผ ์ฆ๊ธฐ์„ธ์š”.', 'JONGNO'), - (6, '๋‹ฌ์ฝคํ•œ ๋””์ €ํŠธ์ƒต', '๋””์ €ํŠธ', 37.5640, 126.9775, '์„œ์šธํŠน๋ณ„์‹œ ๊ฐ•๋‚จ๊ตฌ ๊ฐ•๋‚จ๋Œ€๋กœ 987', '0211111111', - 'https://example.com/store6.jpg', - '10:00:00', '21:00:00', '๋‹ค์–‘ํ•œ ๋””์ €ํŠธ์™€ ์ผ€์ดํฌ๋ฅผ ๋งŒ๋‚˜๋ณด์„ธ์š”.', 'GANGNAM'), - (7, '๋ถ„์œ„๊ธฐ ์ข‹์€ ์ˆ ์ง‘', '์ˆ ์ง‘', 37.5660, 126.9785, '์„œ์šธํŠน๋ณ„์‹œ ์„œ์ดˆ๊ตฌ ์„œ์ดˆ๋Œ€๋กœ 147', '0222222222', - 'https://example.com/store7.jpg', - '18:00:00', '02:00:00', '๋ถ„์œ„๊ธฐ ์ข‹์€ ์ˆ ์ง‘์—์„œ ์ฆ๊ฑฐ์šด ์‹œ๊ฐ„์„ ๋ณด๋‚ด์„ธ์š”.', 'SEOCHO'), - (8, '๋น ๋ฅธ ํŒจ์ŠคํŠธํ‘ธ๋“œ', 'ํŒจ์ŠคํŠธํ‘ธ๋“œ', 37.5680, 126.9795, '์„œ์šธํŠน๋ณ„์‹œ ์†กํŒŒ๊ตฌ ์˜ฌ๋ฆผํ”ฝ๋กœ 258', '0233333333', - 'https://example.com/store8.jpg', - '06:00:00', '23:59:59', '๋น ๋ฅด๊ณ  ๋ง›์žˆ๋Š” ํŒจ์ŠคํŠธํ‘ธ๋“œ๋ฅผ ์ฆ๊ธฐ์„ธ์š”.', 'SONGPA'), - (9, 'ํŽธ๋ฆฌํ•œ ํŽธ์˜์ ', 'ํŽธ์˜์ ', 37.5620, 126.9765, '์„œ์šธํŠน๋ณ„์‹œ ๋งˆํฌ๊ตฌ ํ™๋Œ€๋กœ 369', '0244444444', 'https://example.com/store9.jpg', - '00:00:00', '23:59:59', '24์‹œ๊ฐ„ ํŽธ๋ฆฌํ•œ ํŽธ์˜์ ์ž…๋‹ˆ๋‹ค.', 'MAPO'), - (10, 'ํŠน๋ณ„ํ•œ ๊ธฐํƒ€๋งค์žฅ', '๊ธฐํƒ€', 37.5700, 126.9805, '์„œ์šธํŠน๋ณ„์‹œ ์ข…๋กœ๊ตฌ ์ข…๋กœ 741', '0255555555', - 'https://example.com/store10.jpg', - '09:00:00', '18:00:00', 'ํŠน๋ณ„ํ•œ ์ƒํ’ˆ๋“ค์„ ๋งŒ๋‚˜๋ณด์„ธ์š”.', 'JONGNO') AS new -ON DUPLICATE KEY UPDATE name = new.name, - category = new.category, - latitude = new.latitude, - longitude = new.longitude, - address = new.address, - phone_number = new.phone_number, - image_url = new.image_url, - open_time = new.open_time, - close_time = new.close_time, - introduction = new.introduction, - interest_area = new.interest_area; +INSERT INTO store (id, kakao_id, category, phone_number, name, place_url, road_address, lot_number_address, latitude, + longitude) +VALUES (1, '99999999999', 'KOREAN', '01012345678', '๋ง›์žˆ๋Š” ํ•œ์‹์ง‘', 'https://place.map.kakao.com/17163273', + '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 123-45', '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 123-45', 37.503708148482524, 127.05300772497776), + (2, '99999999998', 'WESTERN', '01087654321', '์•„๋ฆ„๋‹ค์šด ์–‘์‹์ง‘', 'https://place.map.kakao.com/17163273', + '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 67-89', '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 67-89', 37.4979, 127.0276), + (3, '99999999997', 'CHINESE', '01045678912', '์ •ํ†ต ์ค‘์‹๋‹น', 'https://place.map.kakao.com/17163273', + '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 101-112', '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 101-112', 37.56259825108099, 126.97715943361476), + (4, '99999999996', 'WESTERN', '01078912345', '๊ณ ๊ธ‰ ์–‘์‹ ๋ ˆ์Šคํ† ๋ž‘', 'https://place.map.kakao.com/17163273', '', + '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 131-415', 37.4979, 127.0276), + (5, '99999999995', 'ETC', '01034574568', '๋‹ฌ์ฝคํ•œ ๋””์ €ํŠธ ์นดํŽ˜', 'https://place.map.kakao.com/17163273', + '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 161-718', '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 161-718', 37.49491300989233, 127.03150463098274), + (6, '99999999994', 'ETC', '01043609998', '์•„๋Š‘ํ•œ ์ปคํ”ผ์ˆ', 'https://place.map.kakao.com/17163273', + '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 192-021', '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 192-021', 37.5298343127044, 126.919484339847), + (7, '99999999993', 'ETC', '01065083298', '๋น ๋ฅธ ํŒจ์ŠคํŠธํ‘ธ๋“œ์ ', 'https://place.map.kakao.com/17163273', '', + '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 222-324', 37.5036675804016, 127.05305858911); -INSERT INTO menu (id, store_id, name, description, price, discount_price, start_time, end_time, image_url) - VALUES (1, 1, '๋ถˆ๊ณ ๊ธฐ', '๋ง›์žˆ๋Š” ๋ถˆ๊ณ ๊ธฐ', 15000, 12000, '2025-06-28 14:00:00', '2025-06-28 16:00:00', - 'https://example.com/menu1.jpg'), - (2, 1, '๋น„๋น”๋ฐฅ', '์‹ ์„ ํ•œ ์ฑ„์†Œ๊ฐ€ ๋“ค์–ด๊ฐ„ ๋น„๋น”๋ฐฅ', 12000, NULL, NULL, NULL, 'https://example.com/menu2.jpg'), - (3, 2, '์งœ์žฅ๋ฉด', '์ •ํ†ต ์ค‘๊ตญ ์งœ์žฅ๋ฉด', 8000, 6000, '2025-06-28 12:00:00', '2025-06-28 14:00:00', - 'https://example.com/menu3.jpg'), - (4, 2, 'ํƒ•์ˆ˜์œก', '๋ฐ”์‚ญํ•œ ํƒ•์ˆ˜์œก', 18000, NULL, NULL, NULL, 'https://example.com/menu4.jpg'), - (5, 3, '์ดˆ๋ฐฅ ์„ธํŠธ', '์‹ ์„ ํ•œ ํšŒ๋กœ ๋งŒ๋“  ์ดˆ๋ฐฅ ์„ธํŠธ', 25000, 20000, '2025-06-28 15:00:00', '2025-06-28 17:00:00', - 'https://example.com/menu5.jpg'), - (6, 3, '์šฐ๋™', '๋”ฐ๋œปํ•œ ์šฐ๋™', 12000, NULL, NULL, NULL, 'https://example.com/menu6.jpg'), - (7, 4, '๊นŒ๋ฅด๋ณด๋‚˜๋ผ', '์ •ํ†ต ์ดํƒˆ๋ฆฌ์•„ ๊นŒ๋ฅด๋ณด๋‚˜๋ผ', 18000, 15000, '2025-06-28 13:00:00', '2025-06-28 15:00:00', - 'https://example.com/menu7.jpg'), - (8, 4, '๋งˆ๋ฅด๊ฒŒ๋ฆฌํƒ€ ํ”ผ์ž', '์‹ ์„ ํ•œ ๋ชจ์งœ๋ ๋ผ ์น˜์ฆˆ ํ”ผ์ž', 22000, NULL, NULL, NULL, 'https://example.com/menu8.jpg'), - (9, 5, '์•„๋ฉ”๋ฆฌ์นด๋…ธ', '๊นŠ์€ ๋ง›์˜ ์•„๋ฉ”๋ฆฌ์นด๋…ธ', 4500, 3500, '2025-06-28 10:00:00', '2025-06-28 12:00:00', - 'https://example.com/menu9.jpg'), - (10, 5, '์นดํŽ˜๋ผ๋–ผ', '๋ถ€๋“œ๋Ÿฌ์šด ์นดํŽ˜๋ผ๋–ผ', 5500, NULL, NULL, NULL, 'https://example.com/menu10.jpg'), - (11, 6, 'ํ‹ฐ๋ผ๋ฏธ์ˆ˜', '์ง„ํ•œ ์ปคํ”ผํ–ฅ์˜ ํ‹ฐ๋ผ๋ฏธ์ˆ˜', 8000, 6000, '2025-06-28 14:00:00', '2025-06-28 16:00:00', - 'https://example.com/menu11.jpg'), - (12, 6, '์ดˆ์ฝ”์ผ€์ดํฌ', '๋‹ฌ์ฝคํ•œ ์ดˆ์ฝ”์ผ€์ดํฌ', 7000, NULL, NULL, NULL, 'https://example.com/menu12.jpg'), - (13, 7, '์†Œ์ฃผ', '๊น”๋”ํ•œ ์†Œ์ฃผ', 4000, 3000, '2025-06-28 20:00:00', '2025-06-28 22:00:00', - 'https://example.com/menu13.jpg'), - (14, 7, '๋งฅ์ฃผ', '์‹œ์›ํ•œ ๋งฅ์ฃผ', 6000, NULL, NULL, NULL, 'https://example.com/menu14.jpg'), - (15, 8, 'ํ–„๋ฒ„๊ฑฐ ์„ธํŠธ', '๋ง›์žˆ๋Š” ํ–„๋ฒ„๊ฑฐ ์„ธํŠธ', 12000, 9000, '2025-06-28 11:00:00', '2025-06-28 13:00:00', - 'https://example.com/menu15.jpg'), - (16, 8, '์น˜ํ‚จ', '๋ฐ”์‚ญํ•œ ์น˜ํ‚จ', 18000, NULL, NULL, NULL, 'https://example.com/menu16.jpg'), - (17, 9, '์‚ผ๊ฐ๊น€๋ฐฅ', '๋ง›์žˆ๋Š” ์‚ผ๊ฐ๊น€๋ฐฅ', 1500, 1200, '2025-06-28 22:00:00', '2025-06-29 06:00:00', - 'https://example.com/menu17.jpg'), - (18, 9, '์ปคํ”ผ', 'ํŽธ์˜์  ์ปคํ”ผ', 1500, NULL, NULL, NULL, 'https://example.com/menu18.jpg'), - (19, 10, 'ํŠน๋ณ„์ƒํ’ˆA', 'ํŠน๋ณ„ํ•œ ์ƒํ’ˆ A', 50000, 40000, '2025-06-28 10:00:00', '2025-06-28 12:00:00', - 'https://example.com/menu19.jpg'), - (20, 10, 'ํŠน๋ณ„์ƒํ’ˆB', 'ํŠน๋ณ„ํ•œ ์ƒํ’ˆ B', 30000, NULL, NULL, NULL, 'https://example.com/menu20.jpg') AS new -ON DUPLICATE KEY UPDATE store_id = new.store_id, - name = new.name, - description = new.description, - price = new.price, - discount_price = new.discount_price, - start_time = new.start_time, - end_time = new.end_time, - image_url = new.image_url; +INSERT INTO cheer (id, member_id, store_id, description, image_key, is_admin) +VALUES (1, 1, 1, '์ •๋ง ๋ง›์žˆ์–ด์š”! ๊ฐ•์ถ”ํ•ฉ๋‹ˆ๋‹ค!', 'default.jpg', true), + (2, 2, 2, '์„œ๋น„์Šค๊ฐ€ ํ›Œ๋ฅญํ•ด์š”!', 'default.jpg', true), + (3, 3, 3, '์—ฌ๊ธฐ ์Œ์‹์ด ์ •๋ง ๋ง›์žˆ์–ด์š”!', 'default.jpg', true), + (4, 4, 4, '๋ถ„์œ„๊ธฐ๊ฐ€ ๋„ˆ๋ฌด ์ข‹์•„์š”!', 'default.jpg', true), + (5, 5, 5, '๋””์ €ํŠธ๊ฐ€ ์ •๋ง ๋ง›์žˆ์–ด์š”!', 'default.jpg', true), + (6, 6, 6, '์ปคํ”ผ๊ฐ€ ์ •๋ง ๋ง›์žˆ์–ด์š”!', 'default.jpg', false), + (7, 7, 7, 'ํŒจ์ŠคํŠธํ‘ธ๋“œ๊ฐ€ ๋น ๋ฅด๊ณ  ๋ง›์žˆ์–ด์š”!', 'default.jpg', false); -INSERT INTO bookmark (id, member_id, store_id) - VALUES (1, 1, 1), - (2, 1, 3), - (3, 2, 2), - (4, 2, 5), - (5, 3, 4), - (6, 3, 6), - (7, 4, 7), - (8, 4, 9), - (9, 5, 8), - (10, 5, 10), - (11, 1, 5), - (12, 2, 1), - (13, 3, 7), - (14, 4, 2), - (15, 5, 4) AS new -ON DUPLICATE KEY UPDATE member_id = new.member_id, - store_id = new.store_id; +INSERT INTO article (id, title, subtitle, article_url, image_key) +VALUES (1, '์ฒซ ๋ฒˆ์งธ ๊ธฐ์‚ฌ', '์„œ๋ธŒํƒ€์ดํ‹€ 1', 'https://example.com/article1', 'default.jpg'), + (2, '๋‘ ๋ฒˆ์งธ ๊ธฐ์‚ฌ', '์„œ๋ธŒํƒ€์ดํ‹€ 2', 'https://example.com/article2', 'default.jpg'), + (3, '์„ธ ๋ฒˆ์งธ ๊ธฐ์‚ฌ', '์„œ๋ธŒํƒ€์ดํ‹€ 3', 'https://example.com/article3', 'default.jpg'); diff --git a/src/main/resources/db/seed/local/R__local_init_data.sql b/src/main/resources/db/seed/local/R__local_init_data.sql deleted file mode 100644 index f96abd5e..00000000 --- a/src/main/resources/db/seed/local/R__local_init_data.sql +++ /dev/null @@ -1,56 +0,0 @@ -INSERT INTO member (id, social_id, nickname, phone_number, interest_area, opt_in_marketing) - VALUES (1, 123456789, '๋กœ์ปฌ๊ฐœ๋ฐœ์ž', '01012345678', 'GANGNAM', true), - (2, 987654321, '๋‹จ์œ„ํ…Œ์Šคํ„ฐ', '01087654321', 'SEOCHO', false) AS new -ON DUPLICATE KEY UPDATE social_id = new.social_id, - nickname = new.nickname, - phone_number = new.phone_number, - interest_area = new.interest_area, - opt_in_marketing = new.opt_in_marketing; - - -INSERT INTO store (id, name, category, latitude, longitude, address, phone_number, image_url, open_time, close_time, - introduction, interest_area) - VALUES (1, '๋กœ์ปฌ ํ•œ์‹๋‹น', 'ํ•œ์‹', 37.5665, 126.9780, '์„œ์šธํŠน๋ณ„์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123', '0212345678', 'https://example.com/store1.jpg', - '09:00:00', '22:00:00', '๋กœ์ปฌ ํ…Œ์ŠคํŠธ์šฉ ํ•œ์‹๋‹น์ž…๋‹ˆ๋‹ค.', 'GANGNAM'), - (2, '๋กœ์ปฌ ์ค‘์‹๋‹น', '์ค‘์‹', 37.5645, 126.9770, '์„œ์šธํŠน๋ณ„์‹œ ์„œ์ดˆ๊ตฌ ์„œ์ดˆ๋Œ€๋กœ 456', '0287654321', 'https://example.com/store2.jpg', - '11:00:00', '21:00:00', '๋กœ์ปฌ ํ…Œ์ŠคํŠธ์šฉ ์ค‘์‹๋‹น์ž…๋‹ˆ๋‹ค.', 'SEOCHO'), - (3, '๋กœ์ปฌ ์ผ์‹๋‹น', '์ผ์‹', 37.5685, 126.9790, '์„œ์šธํŠน๋ณ„์‹œ ์†กํŒŒ๊ตฌ ์˜ฌ๋ฆผํ”ฝ๋กœ 789', '0245678912', 'https://example.com/store3.jpg', - '11:30:00', '22:30:00', '๋กœ์ปฌ ํ…Œ์ŠคํŠธ์šฉ ์ผ์‹๋‹น์ž…๋‹ˆ๋‹ค.', 'SONGPA') AS new -ON DUPLICATE KEY UPDATE name = new.name, - category = new.category, - latitude = new.latitude, - longitude = new.longitude, - address = new.address, - phone_number = new.phone_number, - image_url = new.image_url, - open_time = new.open_time, - close_time = new.close_time, - introduction = new.introduction, - interest_area = new.interest_area; - - -INSERT INTO menu (id, store_id, name, description, price, discount_price, start_time, end_time, - image_url) - VALUES (1, 1, '๋ถˆ๊ณ ๊ธฐ', '๋ง›์žˆ๋Š” ๋ถˆ๊ณ ๊ธฐ', 15000, 12000, '2025-06-28 14:00:00', '2025-06-28 16:00:00', - 'https://example.com/menu1.jpg'), - (2, 1, '๋น„๋น”๋ฐฅ', '์‹ ์„ ํ•œ ์ฑ„์†Œ๊ฐ€ ๋“ค์–ด๊ฐ„ ๋น„๋น”๋ฐฅ', 12000, NULL, NULL, NULL, 'https://example.com/menu2.jpg'), - (3, 2, '์งœ์žฅ๋ฉด', '์ •ํ†ต ์ค‘๊ตญ ์งœ์žฅ๋ฉด', 8000, 6000, '2025-06-28 12:00:00', '2025-06-28 14:00:00', - 'https://example.com/menu3.jpg'), - (4, 3, '์ดˆ๋ฐฅ ์„ธํŠธ', '์‹ ์„ ํ•œ ํšŒ๋กœ ๋งŒ๋“  ์ดˆ๋ฐฅ ์„ธํŠธ', 25000, 20000, '2025-06-28 15:00:00', '2025-06-28 17:00:00', - 'https://example.com/menu5.jpg') AS new -ON DUPLICATE KEY UPDATE store_id = new.store_id, - name = new.name, - description = new.description, - price = new.price, - discount_price = new.discount_price, - start_time = new.start_time, - end_time = new.end_time, - image_url = new.image_url; - - -INSERT INTO bookmark (id, member_id, store_id) - VALUES (1, 1, 1), - (2, 1, 3), - (3, 2, 2) AS new -ON DUPLICATE KEY UPDATE member_id = new.member_id, - store_id = new.store_id; diff --git a/src/main/resources/db/seed/local/V2__local_init_data.sql b/src/main/resources/db/seed/local/V2__local_init_data.sql new file mode 100644 index 00000000..b9fa13ed --- /dev/null +++ b/src/main/resources/db/seed/local/V2__local_init_data.sql @@ -0,0 +1,39 @@ +INSERT INTO member (id, social_id, email, nickname, phone_number, opt_in_marketing) +VALUES (1, 123456789, 'default1@kakao.com', '์ด์Šน๋กœ', '01012345678', true), + (2, 987654321, 'default2@kakao.com', '์ด์ถฉ์•ˆ', '01087654321', false), + (3, 456789123, 'default3@kakao.com', '์žฅ์ˆ˜๋นˆ', '01045678912', true), + (4, 789123456, 'default4@kakao.com', '์„œ์ค€ํ™˜', '01078912345', true), + (5, 321251287, 'default5@kakao.com', '์‹ ๋ฏผ์„ ', '01034574568', false), + (6, 324569987, 'default6@kakao.com', '๋ฐ•ํฌ์ˆ˜', '01043609998', false), + (7, 323487985, 'default7@kakao.com', 'ํ•˜์•„์–€', '01065083298', false); + +INSERT INTO store (id, kakao_id, category, phone_number, name, place_url, road_address, lot_number_address, latitude, + longitude) +VALUES (1, '99999999999', 'KOREAN', '01012345678', '๋ง›์žˆ๋Š” ํ•œ์‹์ง‘', 'https://place.map.kakao.com/17163273', + '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 123-45', '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 123-45', 37.503708148482524, 127.05300772497776), + (2, '99999999998', 'WESTERN', '01087654321', '์•„๋ฆ„๋‹ค์šด ์–‘์‹์ง‘', 'https://place.map.kakao.com/17163273', + '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 67-89', '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 67-89', 37.4979, 127.0276), + (3, '99999999997', 'CHINESE', '01045678912', '์ •ํ†ต ์ค‘์‹๋‹น', 'https://place.map.kakao.com/17163273', + '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 101-112', '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 101-112', 37.56259825108099, 126.97715943361476), + (4, '99999999996', 'WESTERN', '01078912345', '๊ณ ๊ธ‰ ์–‘์‹ ๋ ˆ์Šคํ† ๋ž‘', 'https://place.map.kakao.com/17163273', '', + '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 131-415', 37.4979, 127.0276), + (5, '99999999995', 'ETC', '01034574568', '๋‹ฌ์ฝคํ•œ ๋””์ €ํŠธ ์นดํŽ˜', 'https://place.map.kakao.com/17163273', + '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 161-718', '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 161-718', 37.49491300989233, 127.03150463098274), + (6, '99999999994', 'ETC', '01043609998', '์•„๋Š‘ํ•œ ์ปคํ”ผ์ˆ', 'https://place.map.kakao.com/17163273', + '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 192-021', '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 192-021', 37.5298343127044, 126.919484339847), + (7, '99999999993', 'ETC', '01065083298', '๋น ๋ฅธ ํŒจ์ŠคํŠธํ‘ธ๋“œ์ ', 'https://place.map.kakao.com/17163273', '', + '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 222-324', 37.5036675804016, 127.05305858911); + +INSERT INTO cheer (id, member_id, store_id, description, image_key, is_admin) +VALUES (1, 1, 1, '์ •๋ง ๋ง›์žˆ์–ด์š”! ๊ฐ•์ถ”ํ•ฉ๋‹ˆ๋‹ค!', 'default.jpg', true), + (2, 2, 2, '์„œ๋น„์Šค๊ฐ€ ํ›Œ๋ฅญํ•ด์š”!', 'default.jpg', true), + (3, 3, 3, '์—ฌ๊ธฐ ์Œ์‹์ด ์ •๋ง ๋ง›์žˆ์–ด์š”!', 'default.jpg', true), + (4, 4, 4, '๋ถ„์œ„๊ธฐ๊ฐ€ ๋„ˆ๋ฌด ์ข‹์•„์š”!', 'default.jpg', true), + (5, 5, 5, '๋””์ €ํŠธ๊ฐ€ ์ •๋ง ๋ง›์žˆ์–ด์š”!', 'default.jpg', true), + (6, 6, 6, '์ปคํ”ผ๊ฐ€ ์ •๋ง ๋ง›์žˆ์–ด์š”!', 'default.jpg', false), + (7, 7, 7, 'ํŒจ์ŠคํŠธํ‘ธ๋“œ๊ฐ€ ๋น ๋ฅด๊ณ  ๋ง›์žˆ์–ด์š”!', 'default.jpg', false); + +INSERT INTO article (id, title, subtitle, article_url, image_key) +VALUES (1, '์ฒซ ๋ฒˆ์งธ ๊ธฐ์‚ฌ', '์„œ๋ธŒํƒ€์ดํ‹€ 1', 'https://example.com/article1', 'default.jpg'), + (2, '๋‘ ๋ฒˆ์งธ ๊ธฐ์‚ฌ', '์„œ๋ธŒํƒ€์ดํ‹€ 2', 'https://example.com/article2', 'default.jpg'), + (3, '์„ธ ๋ฒˆ์งธ ๊ธฐ์‚ฌ', '์„œ๋ธŒํƒ€์ดํ‹€ 3', 'https://example.com/article3', 'default.jpg'); diff --git a/src/test/java/timeeat/DatabaseCleaner.java b/src/test/java/eatda/DatabaseCleaner.java similarity index 99% rename from src/test/java/timeeat/DatabaseCleaner.java rename to src/test/java/eatda/DatabaseCleaner.java index d44f1455..d4d321cd 100644 --- a/src/test/java/timeeat/DatabaseCleaner.java +++ b/src/test/java/eatda/DatabaseCleaner.java @@ -1,12 +1,12 @@ -package timeeat; +package eatda; +import jakarta.persistence.EntityManager; import java.util.List; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.springframework.context.ApplicationContext; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.support.TransactionTemplate; -import jakarta.persistence.EntityManager; public class DatabaseCleaner implements BeforeEachCallback { diff --git a/src/test/java/timeeat/TimeEatApplicationTests.java b/src/test/java/eatda/EatdaApplicationTests.java similarity index 76% rename from src/test/java/timeeat/TimeEatApplicationTests.java rename to src/test/java/eatda/EatdaApplicationTests.java index 57e257ae..6af92919 100644 --- a/src/test/java/timeeat/TimeEatApplicationTests.java +++ b/src/test/java/eatda/EatdaApplicationTests.java @@ -1,10 +1,10 @@ -package timeeat; +package eatda; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -class TimeEatApplicationTests { +class EatdaApplicationTests { @Test void contextLoads() { diff --git a/src/test/java/eatda/client/map/KakaoPropertiesTest.java b/src/test/java/eatda/client/map/KakaoPropertiesTest.java new file mode 100644 index 00000000..54b4f5de --- /dev/null +++ b/src/test/java/eatda/client/map/KakaoPropertiesTest.java @@ -0,0 +1,35 @@ +package eatda.client.map; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class KakaoPropertiesTest { + + @Nested + class ValidateApiKey { + + @ValueSource(strings = {"\n", " "}) + @ParameterizedTest + @NullAndEmptySource + void null_๋˜๋Š”_๋นˆ_๋ฌธ์ž์—ด์ธ_API_ํ‚ค๋Š”_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค(String apiKey) { + assertThatThrownBy(() -> new KakaoProperties(apiKey)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("API key must not be null or blank"); + + } + + @Test + void ์œ ํšจํ•œ_API_ํ‚ค๋Š”_์ •์ƒ์ ์œผ๋กœ_์ƒ์„ฑ๋œ๋‹ค() { + assertThatCode(() -> new KakaoProperties("z116bf75dgh76c253hg7c4b123ab3609")) + .doesNotThrowAnyException(); + } + } + +} diff --git a/src/test/java/eatda/client/map/MapClientTest.java b/src/test/java/eatda/client/map/MapClientTest.java new file mode 100644 index 00000000..c9294bda --- /dev/null +++ b/src/test/java/eatda/client/map/MapClientTest.java @@ -0,0 +1,113 @@ +package eatda.client.map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; + +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.test.web.client.response.MockRestResponseCreators; + +@RestClientTest(MapClient.class) +class MapClientTest { + + @Autowired + private MockRestServiceServer mockServer; + + @Autowired + private MapClient mapClient; + + private void setMockServer(HttpMethod method, String url, String responseBody) { + mockServer.expect(requestTo(startsWith(url))) + .andExpect(method(method)) + .andRespond(MockRestResponseCreators.withSuccess(responseBody, MediaType.APPLICATION_JSON)); + } + + @Nested + class SearchShops { + + @Test + void ๊ฐ€๊ฒŒ_๊ฒ€์ƒ‰์„_ํ• _์ˆ˜_์žˆ๋‹ค() { + String url = "https://dapi.kakao.com/v2/local/search/keyword.json"; + String responseBody = """ + { + "documents": [ + { + "address_name": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋Œ€์น˜๋™ 896-33", + "category_group_code": "FD6", + "category_group_name": "์Œ์‹์ ", + "category_name": "์Œ์‹์  > ํ•œ์‹ > ๊ตญ๋ฐฅ", + "distance": "", + "id": "17163273", + "phone": "02-555-9603", + "place_name": "๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€ ๋ณธ์ ", + "place_url": "http://place.map.kakao.com/17163273", + "road_address_name": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ์„ ๋ฆ‰๋กœ86๊ธธ 40-4", + "x": "127.05300772497776", + "y": "37.503708148482524" + } + ], + "meta": { + "is_end": true, + "pageable_count": 1, + "same_name": { + "keyword": "๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€", + "region": [], + "selected_region": "" + }, + "total_count": 1 + } + }"""; + setMockServer(HttpMethod.GET, url, responseBody); + + List results = mapClient.searchShops("๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€"); + + StoreSearchResult result = results.getFirst(); + assertAll( + () -> assertThat(results).hasSize(1), + () -> assertThat(result.kakaoId()).isEqualTo("17163273"), + () -> assertThat(result.categoryGroupCode()).isEqualTo("FD6"), + () -> assertThat(result.categoryName()).isEqualTo("์Œ์‹์  > ํ•œ์‹ > ๊ตญ๋ฐฅ"), + () -> assertThat(result.phoneNumber()).isEqualTo("02-555-9603"), + () -> assertThat(result.name()).isEqualTo("๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€ ๋ณธ์ "), + () -> assertThat(result.placeUrl()).isEqualTo("http://place.map.kakao.com/17163273"), + () -> assertThat(result.lotNumberAddress()).isEqualTo("์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋Œ€์น˜๋™ 896-33"), + () -> assertThat(result.roadAddress()).isEqualTo("์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ์„ ๋ฆ‰๋กœ86๊ธธ 40-4"), + () -> assertThat(result.latitude()).isEqualTo(37.503708148482524), + () -> assertThat(result.longitude()).isEqualTo(127.05300772497776) + ); + } + + @Test + void ๊ฒ€์ƒ‰_๊ฒฐ๊ณผ๊ฐ€_์—†์„_๊ฒฝ์šฐ_๋นˆ_๋ฆฌ์ŠคํŠธ๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + String url = "https://dapi.kakao.com/v2/local/search/keyword.json"; + String responseBody = """ + { + "documents": [], + "meta": { + "is_end": true, + "pageable_count": 0, + "same_name": { + "keyword": "์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฐ€๊ฒŒ", + "region": [], + "selected_region": "" + }, + "total_count": 0 + } + }"""; + setMockServer(HttpMethod.GET, url, responseBody); + + List results = mapClient.searchShops("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฐ€๊ฒŒ"); + + assertThat(results).isEmpty(); + } + } +} diff --git a/src/test/java/timeeat/client/oauth/OauthClientTest.java b/src/test/java/eatda/client/oauth/OauthClientTest.java similarity index 85% rename from src/test/java/timeeat/client/oauth/OauthClientTest.java rename to src/test/java/eatda/client/oauth/OauthClientTest.java index 2543f0a2..9530db2f 100644 --- a/src/test/java/timeeat/client/oauth/OauthClientTest.java +++ b/src/test/java/eatda/client/oauth/OauthClientTest.java @@ -1,4 +1,4 @@ -package timeeat.client.oauth; +package eatda.client.oauth; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -6,6 +6,8 @@ import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; import java.net.URI; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -15,8 +17,6 @@ import org.springframework.http.MediaType; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.test.web.client.response.MockRestResponseCreators; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; @RestClientTest(OauthClient.class) class OauthClientTest { @@ -30,7 +30,7 @@ class OauthClientTest { @Autowired private OauthProperties properties; - public void setMockServer(HttpMethod method, String uri, String responseBody) { + private void setMockServer(HttpMethod method, String uri, String responseBody) { mockServer.expect(requestTo(uri)) .andExpect(method(method)) .andRespond(MockRestResponseCreators.withSuccess(responseBody, MediaType.APPLICATION_JSON)); @@ -117,15 +117,22 @@ class RequestMemberInformation { void Oauth_ํšŒ์›์ •๋ณด๋ฅผ_์š”์ฒญํ• _์ˆ˜_์žˆ๋‹ค() { setMockServer(HttpMethod.GET, "https://kapi.kakao.com/v2/user/me", """ { - "id":123456789, - "connected_at": "2022-04-11T01:45:28Z", + "id": 123456789, + "connected_at": "2025-07-08T13:31:28Z", + "properties": { + "nickname": "์ด์ถฉ์•ˆ" + }, "kakao_account": { "profile_nickname_needs_agreement": false, - "profile_image_needs_agreement": false, "profile": { - "nickname": "ํ™๊ธธ๋™", + "nickname": "์ด์ถฉ์•ˆ", "is_default_nickname": false - } + }, + "has_email": true, + "email_needs_agreement": false, + "is_email_valid": true, + "is_email_verified": true, + "email": "cnddkscndgus@naver.com" } }"""); OauthToken token = new OauthToken("test-access-token"); @@ -134,7 +141,8 @@ class RequestMemberInformation { assertAll( () -> assertThat(memberInfo.socialId()).isEqualTo(123456789L), - () -> assertThat(memberInfo.nickname()).isEqualTo("ํ™๊ธธ๋™") + () -> assertThat(memberInfo.email()).isEqualTo("cnddkscndgus@naver.com"), + () -> assertThat(memberInfo.nickname()).isEqualTo("์ด์ถฉ์•ˆ") ); } } diff --git a/src/test/java/timeeat/client/oauth/OauthPropertiesTest.java b/src/test/java/eatda/client/oauth/OauthPropertiesTest.java similarity index 97% rename from src/test/java/timeeat/client/oauth/OauthPropertiesTest.java rename to src/test/java/eatda/client/oauth/OauthPropertiesTest.java index 7b317841..568d07c0 100644 --- a/src/test/java/timeeat/client/oauth/OauthPropertiesTest.java +++ b/src/test/java/eatda/client/oauth/OauthPropertiesTest.java @@ -1,14 +1,14 @@ -package timeeat.client.oauth; +package eatda.client.oauth; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import eatda.exception.InitializeException; import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; -import timeeat.exception.InitializeException; class OauthPropertiesTest { diff --git a/src/test/java/eatda/controller/BaseControllerTest.java b/src/test/java/eatda/controller/BaseControllerTest.java new file mode 100644 index 00000000..3827893d --- /dev/null +++ b/src/test/java/eatda/controller/BaseControllerTest.java @@ -0,0 +1,136 @@ +package eatda.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; + +import eatda.DatabaseCleaner; +import eatda.client.map.MapClient; +import eatda.client.map.StoreSearchResult; +import eatda.client.oauth.OauthClient; +import eatda.client.oauth.OauthMemberInformation; +import eatda.client.oauth.OauthToken; +import eatda.controller.web.jwt.JwtManager; +import eatda.domain.member.Member; +import eatda.fixture.CheerGenerator; +import eatda.fixture.MemberGenerator; +import eatda.fixture.StoreGenerator; +import eatda.repository.member.MemberRepository; +import eatda.repository.store.CheerRepository; +import eatda.repository.store.StoreRepository; +import eatda.service.common.ImageService; +import eatda.service.common.ImageService; +import eatda.service.story.StoryService; +import io.restassured.RestAssured; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.filter.Filter; +import io.restassured.filter.log.RequestLoggingFilter; +import io.restassured.filter.log.ResponseLoggingFilter; +import io.restassured.specification.RequestSpecification; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +@ExtendWith(DatabaseCleaner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class BaseControllerTest { + + private static final List SPEC_FILTERS = List.of(new RequestLoggingFilter(), new ResponseLoggingFilter()); + + private static final OauthToken DEFAULT_OAUTH_TOKEN = new OauthToken("oauth-access-token"); + private static final OauthMemberInformation DEFAULT_OAUTH_MEMBER_INFO = + new OauthMemberInformation(314159248183772L, "constant@kakao.com", "nickname"); + private static final String MOCKED_IMAGE_KEY = "mocked-image-path"; + private static final String MOCKED_IMAGE_URL = "https://example.com/image.jpg"; + + + @Autowired + protected MemberGenerator memberGenerator; + + @Autowired + protected StoreGenerator storeGenerator; + + @Autowired + protected CheerGenerator cheerGenerator; + + @Autowired + protected MemberRepository memberRepository; + + @Autowired + protected StoreRepository storeRepository; + + @Autowired + protected CheerRepository cheerRepository; + + @Autowired + protected JwtManager jwtManager; + + @MockitoBean + private OauthClient oauthClient; + + @MockitoBean + private MapClient mapClient; + + @MockitoBean + private ImageService imageService; + + @MockitoBean + protected StoryService storyService; // TODO ์‹ค ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ + + @LocalServerPort + private int port; + + private RequestSpecification spec; + + @BeforeEach + final void setEnvironment() { + RestAssured.port = port; + spec = new RequestSpecBuilder() + .addFilters(SPEC_FILTERS) + .build(); + } + + @BeforeEach + final void mockingClient() throws URISyntaxException { + doReturn(new URI("http://localhost:8080/login/callback")).when(oauthClient).getOauthLoginUrl(anyString()); + doReturn(DEFAULT_OAUTH_TOKEN).when(oauthClient).requestOauthToken(anyString(), anyString()); + doReturn(DEFAULT_OAUTH_MEMBER_INFO).when(oauthClient).requestMemberInformation(DEFAULT_OAUTH_TOKEN); + + List searchResults = List.of( + new StoreSearchResult("123", "FD6", "์Œ์‹์  > ํ•œ์‹ > ๊ตญ๋ฐฅ", "010-1234-1234", "๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€ ๋ณธ์ ", "https://yapp.co.kr", + "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋Œ€์น˜๋™ 896-33", "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ์„ ๋ฆ‰๋กœ86๊ธธ 40-4", 37.0d, 128.0d), + new StoreSearchResult("456", "FD6", "์Œ์‹์  > ํ•œ์‹ > ๊ตญ๋ฐฅ", "010-1234-1234", "๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€ ์‹œ์ฒญ์ ", "http://yapp.kr", + "์„œ์šธ ์ค‘๊ตฌ ๋ถ์ฐฝ๋™ 19-4", null, 37.0d, 128.0d) + ); + doReturn(searchResults).when(mapClient).searchShops(anyString()); + + doReturn(MOCKED_IMAGE_URL).when(imageService).getPresignedUrl(anyString()); + doReturn(MOCKED_IMAGE_KEY).when(imageService).upload(any(), any()); + } + + protected final RequestSpecification given() { + return RestAssured.given(spec); + } + + protected final String accessToken() { + Member member = memberGenerator.generateByEmail(Long.toString(DEFAULT_OAUTH_MEMBER_INFO.socialId()), + "authAccessToken@example.com"); + return jwtManager.issueAccessToken(member.getId()); + } + + protected final String refreshToken() { + Member member = memberGenerator.generateByEmail(Long.toString(DEFAULT_OAUTH_MEMBER_INFO.socialId()), + "authRefreshToken@example.com"); + return jwtManager.issueRefreshToken(member.getId()); + } + + protected final String oauthLoginSocialId() { + return Long.toString(DEFAULT_OAUTH_MEMBER_INFO.socialId()); + } +} diff --git a/src/test/java/timeeat/controller/CorsTest.java b/src/test/java/eatda/controller/CorsTest.java similarity index 98% rename from src/test/java/timeeat/controller/CorsTest.java rename to src/test/java/eatda/controller/CorsTest.java index a5da4acf..14ab18f1 100644 --- a/src/test/java/timeeat/controller/CorsTest.java +++ b/src/test/java/eatda/controller/CorsTest.java @@ -1,4 +1,4 @@ -package timeeat.controller; +package eatda.controller; import static org.hamcrest.Matchers.containsString; diff --git a/src/test/java/timeeat/controller/auth/AuthControllerTest.java b/src/test/java/eatda/controller/auth/AuthControllerTest.java similarity index 97% rename from src/test/java/timeeat/controller/auth/AuthControllerTest.java rename to src/test/java/eatda/controller/auth/AuthControllerTest.java index a6f77b5e..02c8709e 100644 --- a/src/test/java/timeeat/controller/auth/AuthControllerTest.java +++ b/src/test/java/eatda/controller/auth/AuthControllerTest.java @@ -1,14 +1,14 @@ -package timeeat.controller.auth; +package eatda.controller.auth; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; +import eatda.controller.BaseControllerTest; import io.restassured.http.ContentType; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; -import timeeat.controller.BaseControllerTest; class AuthControllerTest extends BaseControllerTest { diff --git a/src/test/java/timeeat/controller/member/MemberControllerTest.java b/src/test/java/eatda/controller/member/MemberControllerTest.java similarity index 68% rename from src/test/java/timeeat/controller/member/MemberControllerTest.java rename to src/test/java/eatda/controller/member/MemberControllerTest.java index c776de29..7c6f5532 100644 --- a/src/test/java/timeeat/controller/member/MemberControllerTest.java +++ b/src/test/java/eatda/controller/member/MemberControllerTest.java @@ -1,16 +1,42 @@ -package timeeat.controller.member; +package eatda.controller.member; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; +import eatda.controller.BaseControllerTest; +import eatda.domain.member.Member; import io.restassured.http.ContentType; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; -import timeeat.controller.BaseControllerTest; class MemberControllerTest extends BaseControllerTest { + @Nested + class GetMember { + + @Test + void ํšŒ์›_์ •๋ณด๋ฅผ_์กฐํšŒํ• _์ˆ˜_์žˆ๋‹ค() { + Member member = memberGenerator.generateRegisteredMember( + "123", "abc@kakao.com", "test-nickname", "01012345678"); + String accessToken = jwtManager.issueAccessToken(member.getId()); + + MemberResponse response = given() + .header(HttpHeaders.AUTHORIZATION, accessToken) + .when().get("/api/member") + .then() + .statusCode(200) + .extract().as(MemberResponse.class); + + assertAll( + () -> assertThat(response.id()).isEqualTo(member.getId()), + () -> assertThat(response.nickname()).isEqualTo(member.getNickname()), + () -> assertThat(response.phoneNumber()).isEqualTo(member.getPhoneNumber()), + () -> assertThat(response.isSignUp()).isFalse() + ); + } + } + @Nested class CheckNickname { @@ -27,7 +53,7 @@ class CheckNickname { @Test void ์ค‘๋ณต๋œ_๋‹‰๋„ค์ž„์„_ํ™•์ธํ• _์ˆ˜_์žˆ๋‹ค() { String existingNickname = "existing-nickname"; - memberGenerator.generateRegisteredMember("123", existingNickname, "01012345678"); + memberGenerator.generateRegisteredMember(existingNickname, "hij@kakao.com", "123", "01012345678"); given() .header(HttpHeaders.AUTHORIZATION, accessToken()) @@ -54,7 +80,7 @@ class CheckPhoneNumber { @Test void ์ค‘๋ณต๋œ_์ „ํ™”๋ฒˆํ˜ธ๋ฅผ_ํ™•์ธํ• _์ˆ˜_์žˆ๋‹ค() { String existingPhoneNumber = "01012345678"; - memberGenerator.generateRegisteredMember("123", "nickname", existingPhoneNumber); + memberGenerator.generateRegisteredMember("nickname", "hij@kakao.com", "123", existingPhoneNumber); given() .header(HttpHeaders.AUTHORIZATION, accessToken()) .queryParam("phoneNumber", existingPhoneNumber) @@ -69,7 +95,7 @@ class UpdateMember { @Test void ํšŒ์›_์ •๋ณด๋ฅผ_์ˆ˜์ •ํ• _์ˆ˜_์žˆ๋‹ค() { - MemberUpdateRequest request = new MemberUpdateRequest("update-nickname", "01012345678", "์„ฑ๋ถ๊ตฌ", true); + MemberUpdateRequest request = new MemberUpdateRequest("update-nickname", "01012345678", true); MemberResponse response = given() .contentType(ContentType.JSON) @@ -84,7 +110,6 @@ class UpdateMember { () -> assertThat(response.isSignUp()).isFalse(), () -> assertThat(response.nickname()).isEqualTo("update-nickname"), () -> assertThat(response.phoneNumber()).isEqualTo("01012345678"), - () -> assertThat(response.interestArea()).isEqualTo("์„ฑ๋ถ๊ตฌ"), () -> assertThat(response.optInMarketing()).isTrue() ); } diff --git a/src/test/java/eatda/controller/store/CheerControllerTest.java b/src/test/java/eatda/controller/store/CheerControllerTest.java new file mode 100644 index 00000000..c0750000 --- /dev/null +++ b/src/test/java/eatda/controller/store/CheerControllerTest.java @@ -0,0 +1,45 @@ +package eatda.controller.store; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import eatda.controller.BaseControllerTest; +import eatda.domain.member.Member; +import eatda.domain.store.Cheer; +import eatda.domain.store.Store; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class CheerControllerTest extends BaseControllerTest { + + @Nested + class GetCheers { + + @Test + void ์š”์ฒญํ•œ_์‘์›_์ค‘_์ตœ์‹ _์‘์›_N๊ฐœ๋ฅผ_์กฐํšŒํ•œ๋‹ค() { + Member member = memberGenerator.generateRegisteredMember("nickname", "ac@kakao.com", "123", "01011111111"); + Store store1 = storeGenerator.generate("111", "์„œ์šธ์‹œ ๋…ธ์›๊ตฌ ์›”๊ณ„3๋™ 123-45"); + Store store2 = storeGenerator.generate("222", "์„œ์šธ์‹œ ์„ฑ๋ถ๊ตฌ ์„๊ด€๋™ 123-45"); + Cheer cheer1 = cheerGenerator.generateAdmin(member, store1); + Cheer cheer2 = cheerGenerator.generateAdmin(member, store1); + Cheer cheer3 = cheerGenerator.generateAdmin(member, store2); + + CheersResponse response = given() + .when() + .queryParam("size", 2) + .get("/api/cheer") + .then() + .statusCode(200) + .extract().as(CheersResponse.class); + + CheerPreviewResponse firstResponse = response.cheers().get(0); + assertAll( + () -> assertThat(response.cheers()).hasSize(2), + () -> assertThat(firstResponse.storeId()).isEqualTo(store2.getId()), + () -> assertThat(firstResponse.storeDistrict()).isEqualTo("์„ฑ๋ถ๊ตฌ"), + () -> assertThat(firstResponse.storeNeighborhood()).isEqualTo("์„๊ด€๋™"), + () -> assertThat(firstResponse.cheerId()).isEqualTo(cheer3.getId()) + ); + } + } +} diff --git a/src/test/java/eatda/controller/store/StoreControllerTest.java b/src/test/java/eatda/controller/store/StoreControllerTest.java new file mode 100644 index 00000000..eac387c2 --- /dev/null +++ b/src/test/java/eatda/controller/store/StoreControllerTest.java @@ -0,0 +1,66 @@ +package eatda.controller.store; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import eatda.controller.BaseControllerTest; +import eatda.domain.member.Member; +import eatda.domain.store.Store; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; + +class StoreControllerTest extends BaseControllerTest { + + @Nested + class GetStores { + + @Test + void ์Œ์‹์ _๋ชฉ๋ก์„_์ตœ์‹ ์ˆœ์œผ๋กœ_์กฐํšŒํ•œ๋‹ค() { + Member member = memberGenerator.generate("111"); + Store store1 = storeGenerator.generate("111", "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋Œ€์น˜๋™ 896-33"); + Store store2 = storeGenerator.generate("222", "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋Œ€์น˜๋™ 896-34"); + Store store3 = storeGenerator.generate("333", "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋Œ€์น˜๋™ 896-35"); + cheerGenerator.generateCommon(member, store1, "image-key-1"); + cheerGenerator.generateCommon(member, store2, "image-key-2"); + cheerGenerator.generateCommon(member, store3, "image-key-3"); + + int size = 2; + + StoresResponse response = given() + .queryParam("size", size) + .when() + .get("/api/shops") + .then() + .statusCode(200) + .extract().as(StoresResponse.class); + + assertAll( + () -> assertThat(response.stores()).hasSize(size), + () -> assertThat(response.stores().get(0).id()).isEqualTo(store3.getId()), + () -> assertThat(response.stores().get(1).id()).isEqualTo(store2.getId()) + ); + } + } + + @Nested + class SearchStores { + + @Test + void ์Œ์‹์ _๊ฒ€์ƒ‰_๊ฒฐ๊ณผ๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + String query = "๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€"; + + StoreSearchResponses responses = given() + .header(HttpHeaders.AUTHORIZATION, accessToken()) + .queryParam("query", query) + .when() + .get("/api/shop/search") + .then() + .statusCode(200) + .extract().as(StoreSearchResponses.class); + + assertThat(responses.stores()).isNotEmpty(); + } + } + +} diff --git a/src/test/java/eatda/controller/story/StoryControllerTest.java b/src/test/java/eatda/controller/story/StoryControllerTest.java new file mode 100644 index 00000000..15af0b4f --- /dev/null +++ b/src/test/java/eatda/controller/story/StoryControllerTest.java @@ -0,0 +1,51 @@ +package eatda.controller.story; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; + +import eatda.controller.BaseControllerTest; +import eatda.service.common.ImageDomain; +import io.restassured.response.Response; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public class StoryControllerTest extends BaseControllerTest { + + @BeforeEach + void setUpMock() { + doNothing() + .when(storyService) + .registerStory(any(), any(), any()); + } + + @Nested + class SearchStores { + + @Test + void ์Šคํ† ๋ฆฌ๋ฅผ_๋“ฑ๋กํ• _์ˆ˜_์žˆ๋‹ค() { + String requestJson = """ + { + "query": "๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€", + "storeKakaoId": "123", + "description": "์—ฌ๊ธฐ ์ง„์งœ ๋ง›์žˆ์–ด์š”!" + } + """; + + byte[] imageBytes = "dummy image content".getBytes(StandardCharsets.UTF_8); + + Response response = given() + .contentType("multipart/form-data") + .header("Authorization", accessToken()) + .multiPart("request", "request.json", requestJson.getBytes(StandardCharsets.UTF_8), "application/json") + .multiPart("image", "image.png", imageBytes, "image/png") + .when() + .post("/api/stories"); + + response.then().statusCode(201); + } + } +} diff --git a/src/test/java/timeeat/controller/web/jwt/JwtManagerTest.java b/src/test/java/eatda/controller/web/jwt/JwtManagerTest.java similarity index 97% rename from src/test/java/timeeat/controller/web/jwt/JwtManagerTest.java rename to src/test/java/eatda/controller/web/jwt/JwtManagerTest.java index c3b6be6f..95a1ef39 100644 --- a/src/test/java/timeeat/controller/web/jwt/JwtManagerTest.java +++ b/src/test/java/eatda/controller/web/jwt/JwtManagerTest.java @@ -1,14 +1,14 @@ -package timeeat.controller.web.jwt; +package eatda.controller.web.jwt; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.junit.jupiter.api.Assertions.assertThrows; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; import java.time.Duration; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; class JwtManagerTest { diff --git a/src/test/java/timeeat/controller/web/jwt/JwtPropertiesTest.java b/src/test/java/eatda/controller/web/jwt/JwtPropertiesTest.java similarity index 96% rename from src/test/java/timeeat/controller/web/jwt/JwtPropertiesTest.java rename to src/test/java/eatda/controller/web/jwt/JwtPropertiesTest.java index 5eca74bb..18b1438f 100644 --- a/src/test/java/timeeat/controller/web/jwt/JwtPropertiesTest.java +++ b/src/test/java/eatda/controller/web/jwt/JwtPropertiesTest.java @@ -1,14 +1,14 @@ -package timeeat.controller.web.jwt; +package eatda.controller.web.jwt; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import eatda.exception.InitializeException; import java.time.Duration; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.NullAndEmptySource; -import timeeat.exception.InitializeException; class JwtPropertiesTest { diff --git a/src/test/java/timeeat/document/BaseDocumentTest.java b/src/test/java/eatda/document/BaseDocumentTest.java similarity index 51% rename from src/test/java/timeeat/document/BaseDocumentTest.java rename to src/test/java/eatda/document/BaseDocumentTest.java index dc3c51bc..3bb276a1 100644 --- a/src/test/java/timeeat/document/BaseDocumentTest.java +++ b/src/test/java/eatda/document/BaseDocumentTest.java @@ -1,12 +1,24 @@ -package timeeat.document; +package eatda.document; +import static org.mockito.Mockito.doReturn; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +import eatda.controller.web.jwt.JwtManager; +import eatda.exception.BusinessErrorCode; +import eatda.exception.EtcErrorCode; +import eatda.service.auth.AuthService; +import eatda.service.common.ImageService; +import eatda.service.member.MemberService; +import eatda.service.store.CheerService; +import eatda.service.store.StoreService; +import eatda.service.story.StoryService; import io.restassured.RestAssured; import io.restassured.builder.RequestSpecBuilder; import io.restassured.specification.RequestSpecification; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.restdocs.RestDocumentationContextProvider; @@ -15,16 +27,18 @@ import org.springframework.restdocs.restassured.RestAssuredRestDocumentationConfigurer; import org.springframework.restdocs.restassured.RestDocumentationFilter; import org.springframework.test.context.bean.override.mockito.MockitoBean; -import timeeat.controller.web.jwt.JwtManager; -import timeeat.service.auth.AuthService; -import timeeat.service.service.MemberService; @ExtendWith({RestDocumentationExtension.class, MockitoExtension.class}) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public abstract class BaseDocumentTest { - @Autowired - private JwtManager jwtManager; + protected static final RestDocsResponse ERROR_RESPONSE = new RestDocsResponse() + .responseBodyField( + fieldWithPath("errorCode").type(STRING).description("์—๋Ÿฌ ์ฝ”๋“œ"), + fieldWithPath("message").type(STRING).description("์—๋Ÿฌ ๋ฉ”์‹œ์ง€") + ); + private static final String MOCKED_ACCESS_TOKEN = "access-token"; + private static final String MOCKED_REFRESH_TOKEN = "refresh-token"; @MockitoBean protected AuthService authService; @@ -32,6 +46,20 @@ public abstract class BaseDocumentTest { @MockitoBean protected MemberService memberService; + @MockitoBean + protected StoreService storeService; + + @MockitoBean + protected StoryService storyService; + + @MockitoBean + protected ImageService imageService; + + @MockitoBean + protected CheerService cheerService; + @MockitoBean + protected JwtManager jwtManager; + @LocalServerPort private int port; @@ -47,6 +75,14 @@ void setEnvironment(RestDocumentationContextProvider restDocumentation) { .build(); } + @BeforeEach + void mockingJwtManager() { + doReturn(MOCKED_ACCESS_TOKEN).when(jwtManager).issueAccessToken(1L); + doReturn(MOCKED_REFRESH_TOKEN).when(jwtManager).issueRefreshToken(1L); + doReturn(1L).when(jwtManager).resolveAccessToken(MOCKED_ACCESS_TOKEN); + doReturn(1L).when(jwtManager).resolveRefreshToken(MOCKED_REFRESH_TOKEN); + } + protected final RestDocsRequest request() { return new RestDocsRequest(); } @@ -59,16 +95,24 @@ protected final RestDocsFilterBuilder document(String identifierPrefix, int stat return new RestDocsFilterBuilder(identifierPrefix, Integer.toString(statusCode)); } + protected final RestDocsFilterBuilder document(String identifierPrefix, BusinessErrorCode errorCode) { + return new RestDocsFilterBuilder(identifierPrefix, errorCode.name()); + } + protected final RequestSpecification given(RestDocumentationFilter documentationFilter) { return RestAssured.given(spec) .filter(documentationFilter); } + protected final RestDocsFilterBuilder document(String identifierPrefix, EtcErrorCode errorCode) { + return new RestDocsFilterBuilder(identifierPrefix, errorCode.name()); + } + protected final String accessToken() { - return jwtManager.issueAccessToken(1L); + return MOCKED_ACCESS_TOKEN; } protected final String refreshToken() { - return jwtManager.issueRefreshToken(1L); + return MOCKED_REFRESH_TOKEN; } } diff --git a/src/test/java/timeeat/document/RestDocsFilterBuilder.java b/src/test/java/eatda/document/RestDocsFilterBuilder.java similarity index 99% rename from src/test/java/timeeat/document/RestDocsFilterBuilder.java rename to src/test/java/eatda/document/RestDocsFilterBuilder.java index 89f61e1d..f6620688 100644 --- a/src/test/java/timeeat/document/RestDocsFilterBuilder.java +++ b/src/test/java/eatda/document/RestDocsFilterBuilder.java @@ -1,7 +1,8 @@ -package timeeat.document; +package eatda.document; import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.document; +import com.epages.restdocs.apispec.ResourceSnippetParametersBuilder; import java.util.ArrayList; import java.util.List; import org.springframework.http.HttpHeaders; @@ -10,7 +11,6 @@ import org.springframework.restdocs.operation.preprocess.Preprocessors; import org.springframework.restdocs.restassured.RestDocumentationFilter; import org.springframework.restdocs.snippet.Snippet; -import com.epages.restdocs.apispec.ResourceSnippetParametersBuilder; public class RestDocsFilterBuilder { diff --git a/src/test/java/timeeat/document/RestDocsRequest.java b/src/test/java/eatda/document/RestDocsRequest.java similarity index 98% rename from src/test/java/timeeat/document/RestDocsRequest.java rename to src/test/java/eatda/document/RestDocsRequest.java index 107219aa..45594238 100644 --- a/src/test/java/timeeat/document/RestDocsRequest.java +++ b/src/test/java/eatda/document/RestDocsRequest.java @@ -1,4 +1,4 @@ -package timeeat.document; +package eatda.document; import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; @@ -6,6 +6,7 @@ import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import com.epages.restdocs.apispec.ResourceSnippetParametersBuilder; import java.util.LinkedList; import java.util.List; import org.springframework.restdocs.cookies.CookieDescriptor; @@ -13,7 +14,6 @@ import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.request.ParameterDescriptor; import org.springframework.restdocs.snippet.Snippet; -import com.epages.restdocs.apispec.ResourceSnippetParametersBuilder; public class RestDocsRequest { diff --git a/src/test/java/timeeat/document/RestDocsResponse.java b/src/test/java/eatda/document/RestDocsResponse.java similarity index 98% rename from src/test/java/timeeat/document/RestDocsResponse.java rename to src/test/java/eatda/document/RestDocsResponse.java index ff74930f..ef142274 100644 --- a/src/test/java/timeeat/document/RestDocsResponse.java +++ b/src/test/java/eatda/document/RestDocsResponse.java @@ -1,4 +1,4 @@ -package timeeat.document; +package eatda.document; import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; diff --git a/src/test/java/timeeat/document/Tag.java b/src/test/java/eatda/document/Tag.java similarity index 71% rename from src/test/java/timeeat/document/Tag.java rename to src/test/java/eatda/document/Tag.java index c821cd8e..61a9c130 100644 --- a/src/test/java/timeeat/document/Tag.java +++ b/src/test/java/eatda/document/Tag.java @@ -1,9 +1,12 @@ -package timeeat.document; +package eatda.document; public enum Tag { AUTH_API("Auth API"), MEMBER_API("Member API"), + STORE_API("Store API"), + STORY_API("Story API"), + CHEER_API("Cheer API"), ; private final String displayName; diff --git a/src/test/java/timeeat/document/auth/AuthDocumentTest.java b/src/test/java/eatda/document/auth/AuthDocumentTest.java similarity index 62% rename from src/test/java/timeeat/document/auth/AuthDocumentTest.java rename to src/test/java/eatda/document/auth/AuthDocumentTest.java index 1815e26b..3477a724 100644 --- a/src/test/java/timeeat/document/auth/AuthDocumentTest.java +++ b/src/test/java/eatda/document/auth/AuthDocumentTest.java @@ -1,7 +1,8 @@ -package timeeat.document.auth; +package eatda.document.auth; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.payload.JsonFieldType.BOOLEAN; import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; @@ -9,20 +10,24 @@ import static org.springframework.restdocs.payload.JsonFieldType.STRING; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import eatda.controller.auth.LoginRequest; +import eatda.controller.auth.ReissueRequest; +import eatda.controller.member.MemberResponse; +import eatda.document.BaseDocumentTest; +import eatda.document.RestDocsRequest; +import eatda.document.RestDocsResponse; +import eatda.document.Tag; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; import io.restassured.http.ContentType; import java.net.URI; import java.net.URISyntaxException; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; -import timeeat.controller.auth.LoginRequest; -import timeeat.controller.auth.ReissueRequest; -import timeeat.controller.member.MemberResponse; -import timeeat.document.BaseDocumentTest; -import timeeat.document.RestDocsRequest; -import timeeat.document.RestDocsResponse; -import timeeat.document.Tag; public class AuthDocumentTest extends BaseDocumentTest { @@ -45,7 +50,7 @@ class RedirectOauthLoginPage { ); @Test - void Oauth_๋กœ๊ทธ์ธ_ํŽ˜์ด์ง€๋กœ_๋ฆฌ๋‹ค์ด๋ ‰ํŠธ_ํ• _์ˆ˜_์žˆ๋‹ค() throws URISyntaxException { + void Oauth_๋กœ๊ทธ์ธ_ํŽ˜์ด์ง€_๋ฆฌ๋‹ค์ด๋ ‰ํŠธ_์„ฑ๊ณต() throws URISyntaxException { doReturn(new URI("http://localhost:8080")).when(authService).getOauthLoginUrl(anyString()); var document = document("auth/oauth-redirect", 302) @@ -61,6 +66,22 @@ class RedirectOauthLoginPage { .then() .statusCode(302); } + + @EnumSource(value = BusinessErrorCode.class, names = {"UNAUTHORIZED_ORIGIN"}) + @ParameterizedTest + void Oauth_๋กœ๊ทธ์ธ_ํŽ˜์ด์ง€_๋ฆฌ๋‹ค์ด๋ ‰ํŠธ_์‹คํŒจ(BusinessErrorCode errorCode) { + doThrow(new BusinessException(errorCode)).when(authService).getOauthLoginUrl(anyString()); + + var document = document("auth/oauth-redirect", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .header(HttpHeaders.REFERER, origin) + .when().get("/api/auth/login/oauth") + .then().statusCode(errorCode.getStatus().value()); + } } @Nested @@ -81,17 +102,17 @@ class Login { fieldWithPath("token.refreshToken").type(STRING).description("๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ"), fieldWithPath("information").type(OBJECT).description("์œ ์ € ์ •๋ณด"), fieldWithPath("information.id").type(NUMBER).description("์œ ์ € ์‹๋ณ„์ž"), + fieldWithPath("information.email").type(STRING).description("์œ ์ € ์ด๋ฉ”์ผ"), fieldWithPath("information.isSignUp").type(BOOLEAN).description("ํšŒ์› ๊ฐ€์ž… ์—ฌ๋ถ€"), fieldWithPath("information.nickname").type(STRING).description("์œ ์ € ๋‹‰๋„ค์ž„"), fieldWithPath("information.phoneNumber").type(STRING).description("ํ•ธ๋“œํฐ ์ „ํ™”๋ฒˆํ˜ธ").optional(), - fieldWithPath("information.interestArea").type(STRING).description("์œ ์ € ๊ด€์‹ฌ ์ง€์—ญ").optional(), fieldWithPath("information.optInMarketing").type(BOOLEAN).description("๋งˆ์ผ€ํŒ… ๋™์˜ ์—ฌ๋ถ€").optional() ); @Test void ๋กœ๊ทธ์ธ_์„ฑ๊ณต() { LoginRequest request = new LoginRequest("code", "http://localhost:3000"); - MemberResponse response = new MemberResponse(1L, true, "๋‹‰๋„ค์ž„", null, null, null); + MemberResponse response = new MemberResponse(1L, "abc@kakao.com", true, "๋‹‰๋„ค์ž„", null, null); doReturn(response).when(authService).login(request); var document = document("auth/login", 201) @@ -101,10 +122,29 @@ class Login { given(document) .contentType(ContentType.JSON) + .header(HttpHeaders.ORIGIN, origin) .body(request) .when().post("/api/auth/login") .then().statusCode(201); } + + @EnumSource(value = BusinessErrorCode.class, names = {"UNAUTHORIZED_ORIGIN", "OAUTH_SERVER_ERROR"}) + @ParameterizedTest + void ๋กœ๊ทธ์ธ_์‹คํŒจ(BusinessErrorCode errorCode) { + LoginRequest request = new LoginRequest("code", "http://localhost:3000"); + doThrow(new BusinessException(errorCode)).when(authService).login(request); + + var document = document("auth/login", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .body(request) + .when().post("/api/auth/login") + .then().statusCode(errorCode.getStatus().value()); + } } @Nested @@ -138,5 +178,23 @@ class ReissueToken { .when().post("/api/auth/reissue") .then().statusCode(201); } + + @EnumSource(value = BusinessErrorCode.class, names = {"EXPIRED_TOKEN", "UNAUTHORIZED_MEMBER"}) + @ParameterizedTest + void ํ† ํฐ_์žฌ๋ฐœ๊ธ‰_์‹คํŒจ(BusinessErrorCode errorCode) { + ReissueRequest request = new ReissueRequest(refreshToken()); + doThrow(new BusinessException(errorCode)).when(jwtManager).resolveRefreshToken(request.refreshToken()); + + var document = document("auth/reissue", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .body(request) + .when().post("/api/auth/reissue") + .then().statusCode(errorCode.getStatus().value()); + } } } diff --git a/src/test/java/eatda/document/member/MemberDocumentTest.java b/src/test/java/eatda/document/member/MemberDocumentTest.java new file mode 100644 index 00000000..ef3203aa --- /dev/null +++ b/src/test/java/eatda/document/member/MemberDocumentTest.java @@ -0,0 +1,251 @@ +package eatda.document.member; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.payload.JsonFieldType.BOOLEAN; +import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; + +import eatda.controller.member.MemberResponse; +import eatda.controller.member.MemberUpdateRequest; +import eatda.document.BaseDocumentTest; +import eatda.document.RestDocsRequest; +import eatda.document.RestDocsResponse; +import eatda.document.Tag; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.http.HttpHeaders; + +public class MemberDocumentTest extends BaseDocumentTest { + + @Nested + class GetMember { + + RestDocsRequest requestDocument = request() + .tag(Tag.MEMBER_API) + .summary("ํšŒ์› ์ •๋ณด ์กฐํšŒ") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("์•ก์„ธ์Šค ํ† ํฐ") + ); + + RestDocsResponse responseDocument = response() + .responseBodyField( + fieldWithPath("id").type(NUMBER).description("ํšŒ์› ์‹๋ณ„์ž"), + fieldWithPath("email").type(STRING).description("ํšŒ์› ์ด๋ฉ”์ผ"), + fieldWithPath("isSignUp").type(BOOLEAN).description("ํšŒ์› ๊ฐ€์ž… ์š”์ฒญ ์—ฌ๋ถ€ (false ๊ณ ์ •)"), + fieldWithPath("nickname").type(STRING).description("ํšŒ์› ๋‹‰๋„ค์ž„").optional(), + fieldWithPath("phoneNumber").type(STRING).description("ํšŒ์› ์ „ํ™”๋ฒˆํ˜ธ ex) 01012345678").optional(), + fieldWithPath("optInMarketing").type(BOOLEAN).description("๋งˆ์ผ€ํŒ… ๋™์˜ ์—ฌ๋ถ€").optional() + ); + + @Test + void ํšŒ์›_์ •๋ณด_์กฐํšŒ_์„ฑ๊ณต() { + MemberResponse response = new MemberResponse(1L, "abc@kakao.com", false, "test-nickname", "01012345678", true); + doReturn(response).when(memberService).getMember(anyLong()); + + var document = document("member/get", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, accessToken()) + .when().get("/api/member") + .then().statusCode(200); + } + + @EnumSource(value = BusinessErrorCode.class, + names = {"UNAUTHORIZED_MEMBER", "EXPIRED_TOKEN", "INVALID_MEMBER_ID"}) + @ParameterizedTest + void ํšŒ์›_์ •๋ณด_์กฐํšŒ_์‹คํŒจ(BusinessErrorCode errorCode) { + doThrow(new BusinessException(errorCode)).when(memberService).getMember(anyLong()); + + var document = document("member/get", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, accessToken()) + .when().get("/api/member") + .then().statusCode(errorCode.getStatus().value()); + } + } + + @Nested + class CheckNickname { + + RestDocsRequest requestDocument = request() + .tag(Tag.MEMBER_API) + .summary("๋‹‰๋„ค์ž„ ์ค‘๋ณต ๊ฒ€์‚ฌ") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("์•ก์„ธ์Šค ํ† ํฐ") + ).queryParameter( + parameterWithName("nickname").description("๊ฒ€์‚ฌํ•  ๋‹‰๋„ค์ž„") + ); + + @Test + void ์ค‘๋ณต_๋‹‰๋„ค์ž„_ํ™•์ธ_์„ฑ๊ณต() { + doNothing().when(memberService).validateNickname(anyString(), anyLong()); + + var document = document("member/nickname-check", 204) + .request(requestDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, accessToken()) + .queryParam("nickname", "new-nickname") + .when().get("/api/member/nickname/check") + .then().statusCode(204); + } + + @EnumSource(value = BusinessErrorCode.class, + names = {"UNAUTHORIZED_MEMBER", "EXPIRED_TOKEN", "DUPLICATE_NICKNAME"}) + @ParameterizedTest + void ์ค‘๋ณต_๋‹‰๋„ค์ž„_ํ™•์ธ_์‹คํŒจ(BusinessErrorCode errorCode) { + doThrow(new BusinessException(errorCode)) + .when(memberService).validateNickname(anyString(), anyLong()); + + var document = document("member/nickname-check", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, accessToken()) + .queryParam("nickname", "existing-nickname") + .when().get("/api/member/nickname/check") + .then().statusCode(errorCode.getStatus().value()); + } + } + + @Nested + class CheckPhoneNumber { + + RestDocsRequest requestDocument = request() + .tag(Tag.MEMBER_API) + .summary("์ „ํ™”๋ฒˆํ˜ธ ์ค‘๋ณต ๊ฒ€์‚ฌ") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("์•ก์„ธ์Šค ํ† ํฐ") + ).queryParameter( + parameterWithName("phoneNumber").description("๊ฒ€์‚ฌํ•  ์ „ํ™”๋ฒˆํ˜ธ ex) 01012345678") + ); + + @Test + void ์ค‘๋ณต_์ „ํ™”๋ฒˆํ˜ธ_ํ™•์ธ_์„ฑ๊ณต() { + doNothing().when(memberService).validatePhoneNumber(anyString(), anyLong()); + + var document = document("member/phone-number-check", 204) + .request(requestDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, accessToken()) + .queryParam("phoneNumber", "01098765432") + .when().get("/api/member/phone-number/check") + .then().statusCode(204); + } + + @EnumSource(value = BusinessErrorCode.class, + names = {"UNAUTHORIZED_MEMBER", "EXPIRED_TOKEN", "DUPLICATE_PHONE_NUMBER"}) + @ParameterizedTest + void ์ค‘๋ณต_์ „ํ™”๋ฒˆํ˜ธ_ํ™•์ธ_์‹คํŒจ(BusinessErrorCode errorCode) { + doThrow(new BusinessException(errorCode)) + .when(memberService).validatePhoneNumber(anyString(), anyLong()); + + var document = document("member/phone-number-check", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, accessToken()) + .queryParam("phoneNumber", "01012345678") + .when().get("/api/member/phone-number/check") + .then().statusCode(errorCode.getStatus().value()); + } + } + + @Nested + class UpdateMember { + + RestDocsRequest requestDocument = request() + .tag(Tag.MEMBER_API) + .summary("ํšŒ์› ์ •๋ณด ์ˆ˜์ •") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("์•ก์„ธ์Šค ํ† ํฐ") + ).requestBodyField( + fieldWithPath("nickname").type(STRING).description("ํšŒ์› ๋‹‰๋„ค์ž„"), + fieldWithPath("phoneNumber").type(STRING).description("ํšŒ์› ์ „ํ™”๋ฒˆํ˜ธ ex) 01012345678"), + fieldWithPath("optInMarketing").type(BOOLEAN).description("๋งˆ์ผ€ํŒ… ๋™์˜ ์—ฌ๋ถ€") + ); + + RestDocsResponse responseDocument = response() + .responseBodyField( + fieldWithPath("id").type(NUMBER).description("ํšŒ์› ์‹๋ณ„์ž"), + fieldWithPath("email").type(STRING).description("ํšŒ์› ์ด๋ฉ”์ผ"), + fieldWithPath("isSignUp").type(BOOLEAN).description("ํšŒ์› ๊ฐ€์ž… ์š”์ฒญ ์—ฌ๋ถ€ (false ๊ณ ์ •)"), + fieldWithPath("nickname").type(STRING).description("ํšŒ์› ๋‹‰๋„ค์ž„").optional(), + fieldWithPath("phoneNumber").type(STRING).description("ํšŒ์› ์ „ํ™”๋ฒˆํ˜ธ ex) 01012345678").optional(), + fieldWithPath("optInMarketing").type(BOOLEAN).description("๋งˆ์ผ€ํŒ… ๋™์˜ ์—ฌ๋ถ€").optional() + ); + + @Test + void ํšŒ์›_์ •๋ณด_์ˆ˜์ •_์„ฑ๊ณต() { + MemberUpdateRequest request = new MemberUpdateRequest("update-nickname", "01012345678", true); + MemberResponse response = new MemberResponse(1L, "abc@kakao.com", false, "update-nickname", "01012345678", true); + doReturn(response).when(memberService).update(anyLong(), eq(request)); + + var document = document("member/update", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, accessToken()) + .body(request) + .when().put("/api/member") + .then().statusCode(200); + } + + @EnumSource(value = BusinessErrorCode.class, + names = {"UNAUTHORIZED_MEMBER", "EXPIRED_TOKEN", "DUPLICATE_NICKNAME", "DUPLICATE_PHONE_NUMBER", + "INVALID_MOBILE_PHONE_NUMBER", "INVALID_MARKETING_CONSENT"}) + @ParameterizedTest + void ํšŒ์›_์ •๋ณด_์ˆ˜์ •_์‹คํŒจ(BusinessErrorCode errorCode) { + MemberUpdateRequest request = new MemberUpdateRequest("update-nickname", "01012345678", true); + doThrow(new BusinessException(errorCode)).when(memberService).update(anyLong(), eq(request)); + + var document = document("member/update", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, accessToken()) + .body(request) + .when().put("/api/member") + .then().statusCode(errorCode.getStatus().value()); + } + } +} diff --git a/src/test/java/eatda/document/store/CheerDocumentTest.java b/src/test/java/eatda/document/store/CheerDocumentTest.java new file mode 100644 index 00000000..e8dccd8e --- /dev/null +++ b/src/test/java/eatda/document/store/CheerDocumentTest.java @@ -0,0 +1,93 @@ +package eatda.document.store; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.springframework.restdocs.payload.JsonFieldType.ARRAY; +import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; + +import eatda.controller.store.CheerPreviewResponse; +import eatda.controller.store.CheersResponse; +import eatda.document.BaseDocumentTest; +import eatda.document.RestDocsRequest; +import eatda.document.RestDocsResponse; +import eatda.document.Tag; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; +import io.restassured.http.ContentType; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +public class CheerDocumentTest extends BaseDocumentTest { + + @Nested + class Get { + + RestDocsRequest requestDocument = request() + .tag(Tag.STORE_API) + .summary("์ตœ์‹  ์‘์› ๊ฒ€์ƒ‰") + .queryParameter( + parameterWithName("size").description("์กฐํšŒ ๊ฐœ์ˆ˜ (์ตœ์†Œ 1, ์ตœ๋Œ€ 50)") + ); + + RestDocsResponse responseDocument = response() + .responseBodyField( + fieldWithPath("cheers").type(ARRAY).description("์‘์› ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ"), + fieldWithPath("cheers[].storeId").type(NUMBER).description("๊ฐ€๊ฒŒ ID"), + fieldWithPath("cheers[].imageUrl").type(STRING).description("์ด๋ฏธ์ง€ URL").optional(), + fieldWithPath("cheers[].storeName").type(STRING).description("๊ฐ€๊ฒŒ ์ด๋ฆ„"), + fieldWithPath("cheers[].storeDistrict").type(STRING).description("๊ฐ€๊ฒŒ ์ฃผ์†Œ (๊ตฌ)"), + fieldWithPath("cheers[].storeNeighborhood").type(STRING).description("๊ฐ€๊ฒŒ ์ฃผ์†Œ (๋™)"), + fieldWithPath("cheers[].storeCategory").type(STRING).description("๊ฐ€๊ฒŒ ์นดํ…Œ๊ณ ๋ฆฌ"), + fieldWithPath("cheers[].cheerId").type(NUMBER).description("์‘์› ID"), + fieldWithPath("cheers[].cheerDescription").type(STRING).description("์‘์› ๋‚ด์šฉ") + ); + + @Test + void ์Œ์‹์ _๊ฒ€์ƒ‰_์„ฑ๊ณต() { + int size = 2; + CheersResponse responses = new CheersResponse(List.of( + new CheerPreviewResponse(2L, "https://example.image", "๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€ ๋ณธ์ ", "๊ฐ•๋‚จ๊ตฌ", "์„ ๋ฆ‰๊ตฌ", "ํ•œ์‹", 2L, + "๋„ˆ๋ฌด ๋ง›์žˆ์–ด์š”!"), + new CheerPreviewResponse(1L, null, "์„๊ด€๋™๋–ก๋ณถ์ด", "์„ฑ๋ถ๊ตฌ", "์„๊ด€๋™", "๊ธฐํƒ€", 1L, + "๋„ˆ๋ฌด ๋งค์›Œ์š”! ํ•˜์ง€๋งŒ ๋ง›์žˆ์–ด์š”!") + )); + doReturn(responses).when(cheerService).getCheers(anyInt()); + + var document = document("cheer/get-many", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .queryParam("size", size) + .when().get("/api/cheer") + .then().statusCode(200); + } + + @EnumSource(value = BusinessErrorCode.class, names = {"PRESIGNED_URL_GENERATION_FAILED"}) + @ParameterizedTest + void ์Œ์‹์ _๊ฒ€์ƒ‰_์‹คํŒจ(BusinessErrorCode errorCode) { + int size = 2; + doThrow(new BusinessException(errorCode)).when(cheerService).getCheers(anyInt()); + + var document = document("cheer/get-many", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .queryParam("size", size) + .when().get("/api/cheer") + .then().statusCode(errorCode.getStatus().value()); + } + } +} diff --git a/src/test/java/eatda/document/store/StoreDocumentTest.java b/src/test/java/eatda/document/store/StoreDocumentTest.java new file mode 100644 index 00000000..31210808 --- /dev/null +++ b/src/test/java/eatda/document/store/StoreDocumentTest.java @@ -0,0 +1,158 @@ +package eatda.document.store; + + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.payload.JsonFieldType.ARRAY; +import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; + +import eatda.controller.store.StorePreviewResponse; +import eatda.controller.store.StoreSearchResponse; +import eatda.controller.store.StoreSearchResponses; +import eatda.controller.store.StoresResponse; +import eatda.document.BaseDocumentTest; +import eatda.document.RestDocsRequest; +import eatda.document.RestDocsResponse; +import eatda.document.Tag; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; +import io.restassured.http.ContentType; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.http.HttpHeaders; + +public class StoreDocumentTest extends BaseDocumentTest { + + @Nested + class GetStores { + + RestDocsRequest requestDocument = request() + .tag(Tag.STORE_API) + .summary("์Œ์‹์  ๋ชฉ๋ก ์กฐํšŒ") + .queryParameter( + parameterWithName("size").description("์กฐํšŒํ•  ์Œ์‹์  ๊ฐœ์ˆ˜ (์ตœ์†Œ 1, ์ตœ๋Œ€ 50)") + ); + + RestDocsResponse responseDocument = response() + .responseBodyField( + fieldWithPath("stores").type(ARRAY).description("์Œ์‹์  ๋ชฉ๋ก"), + fieldWithPath("stores[].id").type(NUMBER).description("์Œ์‹์  ID"), + fieldWithPath("stores[].imageUrl").type(STRING).description("์Œ์‹์  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ URL"), + fieldWithPath("stores[].name").type(STRING).description("์Œ์‹์  ์ด๋ฆ„"), + fieldWithPath("stores[].district").type(STRING).description("์Œ์‹์  ์ฃผ์†Œ (๊ตฌ)"), + fieldWithPath("stores[].neighborhood").type(STRING).description("์Œ์‹์  ์ฃผ์†Œ (๋™)"), + fieldWithPath("stores[].category").type(STRING).description("์Œ์‹์  ์นดํ…Œ๊ณ ๋ฆฌ") + ); + + @Test + void ์Œ์‹์ _๋ชฉ๋ก_์ตœ์‹ ์ˆœ์œผ๋กœ_์กฐํšŒ() { + StoresResponse response = new StoresResponse(List.of( + new StorePreviewResponse(2L, "https://example.image", "๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€", "๊ฐ•๋‚จ๊ตฌ", "๋Œ€์น˜๋™", "ํ•œ์‹"), + new StorePreviewResponse(1L, "https://example.image", "์„๊ด€๋™๋–ก๋ณถ์ด", "์„ฑ๋ถ๊ตฌ", "์„๊ด€๋™", "ํ•œ์‹") + )); + doReturn(response).when(storeService).getStores(anyInt()); + + int size = 2; + var document = document("store/get", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .queryParam("size", size) + .when().get("/api/shops") + .then().statusCode(200); + } + + @EnumSource(value = BusinessErrorCode.class, names = {"PRESIGNED_URL_GENERATION_FAILED"}) + @ParameterizedTest + void ์Œ์‹์ _๋ชฉ๋ก_์กฐํšŒ_์‹คํŒจ(BusinessErrorCode errorCode) { + doThrow(new BusinessException(errorCode)).when(storeService).getStores(anyInt()); + + int size = 2; + var document = document("store/get", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .queryParam("size", size) + .when().get("/api/shops") + .then().statusCode(errorCode.getStatus().value()); + } + } + + @Nested + class SearchStores { + + RestDocsRequest requestDocument = request() + .tag(Tag.STORE_API) + .summary("์Œ์‹์  ๊ฒ€์ƒ‰") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("์•ก์„ธ์Šค ํ† ํฐ") + ).queryParameter( + parameterWithName("query").description("๊ฒ€์ƒ‰์–ด") + ); + + RestDocsResponse responseDocument = response() + .responseBodyField( + fieldWithPath("stores").type(ARRAY).description("์Œ์‹์  ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ"), + fieldWithPath("stores[].kakaoId").type(STRING).description("์นด์นด์˜ค ์Œ์‹์  ID"), + fieldWithPath("stores[].name").type(STRING).description("์Œ์‹์  ์ด๋ฆ„"), + fieldWithPath("stores[].address").type(STRING).description("์Œ์‹์  ์ฃผ์†Œ (์ง€๋ฒˆ ์ฃผ์†Œ)") + ); + + @Test + void ์Œ์‹์ _๊ฒ€์ƒ‰_์„ฑ๊ณต() { + String query = "๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€"; + StoreSearchResponses responses = new StoreSearchResponses(List.of( + new StoreSearchResponse("17163273", "๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€ ๋ณธ์ ", "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋Œ€์น˜๋™ 896-33"), + new StoreSearchResponse("1062153333", "๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€ ์‹œ์ฒญ์ง์˜์ ", "์„œ์šธ ์ค‘๊ตฌ ๋ถ์ฐฝ๋™ 19-4") + )); + doReturn(responses).when(storeService).searchStores(anyString()); + + var document = document("store/search", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, accessToken()) + .queryParam("query", query) + .when().get("/api/shop/search") + .then().statusCode(200); + } + + @EnumSource(value = BusinessErrorCode.class, + names = {"UNAUTHORIZED_MEMBER", "EXPIRED_TOKEN", "MAP_SERVER_ERROR"}) + @ParameterizedTest + void ์Œ์‹์ _๊ฒ€์ƒ‰_์‹คํŒจ(BusinessErrorCode errorCode) { + String query = "๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€"; + doThrow(new BusinessException(errorCode)).when(storeService).searchStores(anyString()); + + var document = document("store/search", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, accessToken()) + .queryParam("query", query) + .when().get("/api/shop/search") + .then().statusCode(errorCode.getStatus().value()); + } + } +} diff --git a/src/test/java/eatda/document/story/StoryDocumentTest.java b/src/test/java/eatda/document/story/StoryDocumentTest.java new file mode 100644 index 00000000..e88838b7 --- /dev/null +++ b/src/test/java/eatda/document/story/StoryDocumentTest.java @@ -0,0 +1,136 @@ +package eatda.document.story; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; + +import eatda.document.BaseDocumentTest; +import eatda.document.RestDocsRequest; +import eatda.document.RestDocsResponse; +import eatda.document.Tag; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; +import eatda.exception.EtcErrorCode; +import eatda.service.common.ImageDomain; +import io.restassured.response.Response; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.restassured.RestDocumentationFilter; + +public class StoryDocumentTest extends BaseDocumentTest { + + @Nested + class RegisterStory { + + RestDocsRequest requestDocument = request() + .tag(Tag.STORY_API) + .summary("์Šคํ† ๋ฆฌ ๋“ฑ๋ก") + .description("์Šคํ† ๋ฆฌ์™€ ์ด๋ฏธ์ง€๋ฅผ multipart/form-data๋กœ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("์•ก์„ธ์Šค ํ† ํฐ") + ); + + RestDocsResponse responseDocument = response(); + + @Test + void ์Šคํ† ๋ฆฌ_๋“ฑ๋ก_์„ฑ๊ณต() { + doReturn("https://dummy-s3.com/story.png") + .when(imageService) + .upload(any(), org.mockito.ArgumentMatchers.eq(ImageDomain.STORY)); + + doNothing().when(storyService) + .registerStory(any(), any(), any()); + + String requestJson = """ + { + "query": "๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€", + "storeKakaoId": "123", + "description": "์—ฌ๊ธฐ ์ง„์งœ ๋ง›์žˆ์–ด์š”!" + } + """; + + byte[] imageBytes = "dummy image content".getBytes(StandardCharsets.UTF_8); + + RestDocumentationFilter document = document("story/register", 201) + .request(requestDocument) + .response(responseDocument) + .build(); + + Response response = given(document) + .contentType("multipart/form-data") + .header(HttpHeaders.AUTHORIZATION, accessToken()) + .multiPart("request", "request.json", requestJson.getBytes(StandardCharsets.UTF_8), "application/json") + .multiPart("image", "image.png", imageBytes, "image/png") + .when().post("/api/stories"); + + response.then().statusCode(201); + } + + @Test + void ์Šคํ† ๋ฆฌ_๋“ฑ๋ก_์‹คํŒจ_ํ•„์ˆ˜๊ฐ’_๋ˆ„๋ฝ() { + String invalidJson = """ + { + "query": "๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€", + "storeKakaoId": "123" + } + """; + + byte[] imageBytes = "dummy image content".getBytes(StandardCharsets.UTF_8); + + doThrow(new BusinessException(BusinessErrorCode.INVALID_STORY_DESCRIPTION)) + .when(storyService) + .registerStory(any(), any(), any()); + + var document = document("story/register", EtcErrorCode.CLIENT_REQUEST_ERROR) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType("multipart/form-data") + .header(HttpHeaders.AUTHORIZATION, accessToken()) + .multiPart("request", "request.json", invalidJson.getBytes(StandardCharsets.UTF_8), "application/json") + .multiPart("image", "image.png", imageBytes, "image/png") + .when().post("/api/stories") + .then().statusCode(EtcErrorCode.CLIENT_REQUEST_ERROR.getStatus().value()); + } + + @Test + void ์Šคํ† ๋ฆฌ_๋“ฑ๋ก_์‹คํŒจ_์ด๋ฏธ์ง€_ํ˜•์‹_์˜ค๋ฅ˜() { + String requestJson = """ + { + "query": "๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€", + "storeKakaoId": "123", + "description": "์—ฌ๊ธฐ ์ง„์งœ ๋ง›์žˆ์–ด์š”!" + } + """; + + byte[] invalidImage = "not an image".getBytes(StandardCharsets.UTF_8); + + doThrow(new BusinessException(BusinessErrorCode.INVALID_IMAGE_TYPE)) + .when(storyService) + .registerStory(any(), any(), any()); + + var document = document("story/register", BusinessErrorCode.INVALID_IMAGE_TYPE) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + Response response = given(document) + .contentType("multipart/form-data") + .header(HttpHeaders.AUTHORIZATION, accessToken()) + .multiPart("request", "request.json", requestJson.getBytes(StandardCharsets.UTF_8), "application/json") + .multiPart("image", "image.txt", invalidImage, "text/plain") + .when().post("/api/stories"); + + System.out.println("์‘๋‹ต ์ƒํƒœ์ฝ”๋“œ >>> " + response.statusCode()); + System.out.println("์‘๋‹ต ๋ฐ”๋”” >>> " + response.asString()); + + response.then().statusCode(BusinessErrorCode.INVALID_IMAGE_TYPE.getStatus().value()); + } + } +} diff --git a/src/test/java/timeeat/domain/member/MemberTest.java b/src/test/java/eatda/domain/member/MemberTest.java similarity index 68% rename from src/test/java/timeeat/domain/member/MemberTest.java rename to src/test/java/eatda/domain/member/MemberTest.java index f7268066..aedd5ca9 100644 --- a/src/test/java/timeeat/domain/member/MemberTest.java +++ b/src/test/java/eatda/domain/member/MemberTest.java @@ -1,15 +1,17 @@ -package timeeat.domain.member; +package eatda.domain.member; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertThrows; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import timeeat.enums.InterestArea; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; class MemberTest { @@ -18,16 +20,17 @@ class MemberTest { class CreateOauthMember { @Test - void socialId์™€_nickname๋งŒ์œผ๋กœ_์ƒ์„ฑ_์‹œ_๋‚˜๋จธ์ง€_ํ•„๋“œ๋Š”_null_์ƒํƒœ์ด๋‹ค() { + void socialId_email_nickname๋งŒ์œผ๋กœ_์ƒ์„ฑ_์‹œ_๋‚˜๋จธ์ง€_ํ•„๋“œ๋Š”_null_์ƒํƒœ์ด๋‹ค() { String socialId = "oauth-user-id"; + String email = "test@example.com"; - Member member = new Member(socialId, "nickname"); + Member member = new Member(socialId, email, "nickname"); assertAll( () -> assertThat(member.getSocialId()).isEqualTo(socialId), + () -> assertThat(member.getEmail()).isEqualTo(email), () -> assertThat(member.getNickname()).isNotNull(), () -> assertThat(member.getMobilePhoneNumber()).isNull(), - () -> assertThat(member.getInterestArea()).isNull(), () -> assertThat(member.getOptInMarketing()).isNull() ); } @@ -35,11 +38,25 @@ class CreateOauthMember { @Test void socialId๊ฐ€_null์ด๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { String socialId = null; + String email = "test@example.com"; - BusinessException exception = assertThrows(BusinessException.class, () -> new Member(socialId, "nickname")); + BusinessException exception = assertThrows(BusinessException.class, + () -> new Member(socialId, email, "nickname")); assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_SOCIAL_ID); } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"abc.com", "invalid-email", "test@.com", "@example.com"}) + void email์ด_null์ด๊ฑฐ๋‚˜_์œ ํšจํ•˜์ง€_์•Š์œผ๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค(String invalidEmail) { + String socialId = "oauth-user-id"; + + BusinessException exception = assertThrows(BusinessException.class, + () -> new Member(socialId, invalidEmail, "nickname")); + + assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_EMAIL); + } } @Nested @@ -49,18 +66,18 @@ class CreateMemberTest { @Test void ๋ชจ๋“ _์ •๋ณด๊ฐ€_์ •์ƒ์ ์ผ_๋•Œ_ํšŒ์›์„_์ƒ์„ฑํ•œ๋‹ค() { String socialId = "test-social-id-123"; + String email = "test@example.com"; String nickname = "๋ง›์žˆ๋Š”๋…€์„๋“ค-32"; String mobilePhoneNumber = "01012345678"; - String interestArea = "๊ฐ•๋‚จ๊ตฌ"; Boolean optInMarketing = true; - Member member = new Member(socialId, nickname, mobilePhoneNumber, interestArea, optInMarketing); + Member member = new Member(socialId, email, nickname, mobilePhoneNumber, optInMarketing); assertAll( () -> assertThat(member.getSocialId()).isEqualTo(socialId), + () -> assertThat(member.getEmail()).isEqualTo(email), () -> assertThat(member.getNickname()).isEqualTo(nickname), () -> assertThat(member.getMobilePhoneNumber().getValue()).isEqualTo(mobilePhoneNumber), - () -> assertThat(member.getInterestArea()).isEqualTo(InterestArea.GANGNAM), () -> assertThat(member.isOptInMarketing()).isTrue() ); } @@ -68,12 +85,12 @@ class CreateMemberTest { @Test void ์„ ํƒ์ _ํ•„๋“œ๊ฐ€_null์ด์–ด๋„_๋ฉค๋ฒ„๋ฅผ_์ƒ์„ฑํ•œ๋‹ค() { String socialId = "test-social-id-123"; + String email = "test@example.com"; String nickname = null; String mobilePhoneNumber = null; - String interestArea = "๊ฐ•๋‚จ๊ตฌ"; Boolean optInMarketing = false; - Member member = new Member(socialId, nickname, mobilePhoneNumber, interestArea, optInMarketing); + Member member = new Member(socialId, email, nickname, mobilePhoneNumber, optInMarketing); assertAll( () -> assertThat(member.getNickname()).isNull(), @@ -85,30 +102,16 @@ class CreateMemberTest { @Test void ๊ฐ€์ž…_์™„๋ฃŒ_์‹œ_๋งˆ์ผ€ํŒ…_๋™์˜_์—ฌ๋ถ€๊ฐ€_null์ด๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { String socialId = "test-social-id-123"; + String email = "test@example.com"; String nickname = "๋ง›์žˆ๋Š”๋…€์„๋“ค-32"; String mobilePhoneNumber = "01012345678"; - String interestArea = "๊ฐ•๋‚จ๊ตฌ"; Boolean optInMarketing = null; BusinessException exception = assertThrows(BusinessException.class, - () -> new Member(socialId, nickname, mobilePhoneNumber, interestArea, optInMarketing)); + () -> new Member(socialId, email, nickname, mobilePhoneNumber, optInMarketing)); assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_MARKETING_CONSENT); } - - @Test - void ์œ ํšจํ•˜์ง€_์•Š์€_๊ด€์‹ฌ_์ง€์—ญ์œผ๋กœ_์ƒ์„ฑํ•˜๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - String socialId = "test-social-id-123"; - String nickname = "๋ง›์žˆ๋Š”๋…€์„๋“ค-32"; - String mobilePhoneNumber = "01012345678"; - String interestArea = "๋ถ€์‚ฐ"; - Boolean optInMarketing = true; - - BusinessException exception = assertThrows(BusinessException.class, - () -> new Member(socialId, nickname, mobilePhoneNumber, interestArea, optInMarketing)); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_INTEREST_AREA); - } } @Nested @@ -116,15 +119,14 @@ class UpdateMember { @Test void ํšŒ์›_์ •๋ณด๋ฅผ_์ •์ƒ์ ์œผ๋กœ_์ˆ˜์ •ํ•œ๋‹ค() { - Member member = new Member("social-id", "nickname"); - Member updatedMember = new Member("new-nickname", "01012345678", "๊ฐ•๋‚จ๊ตฌ", true); + Member member = new Member("social-id", "abc@example.com", "nickname"); + Member updatedMember = new Member("new-nickname", "01012345678", true); member.update(updatedMember); assertAll( () -> assertThat(member.getNickname()).isEqualTo("new-nickname"), () -> assertThat(member.getPhoneNumber()).isEqualTo("01012345678"), - () -> assertThat(member.getInterestArea()).isEqualTo(InterestArea.GANGNAM), () -> assertThat(member.isOptInMarketing()).isTrue() ); } @@ -135,7 +137,7 @@ class IsSameNicknameTest { @Test void ๋™์ผํ•œ_๋‹‰๋„ค์ž„์„_๋น„๊ตํ•˜๋ฉด_true๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { - Member member = new Member("social-id", "nickname"); + Member member = new Member("social-id", "abc@example.com", "nickname"); boolean result = member.isSameNickname("nickname"); @@ -144,7 +146,7 @@ class IsSameNicknameTest { @Test void ๋‹ค๋ฅธ_๋‹‰๋„ค์ž„์„_๋น„๊ตํ•˜๋ฉด_false๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { - Member member = new Member("social-id", "nickname"); + Member member = new Member("social-id", "abc@example.com", "nickname"); boolean result = member.isSameNickname("different-nickname"); @@ -157,7 +159,7 @@ class IsSameMobilePhoneNumberTest { @Test void ๋™์ผํ•œ_์ „ํ™”๋ฒˆํ˜ธ๋ฅผ_๋น„๊ตํ•˜๋ฉด_true๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { - Member member = new Member("social-id", "nickname", "01012345678", "๊ฐ•๋‚จ๊ตฌ", true); + Member member = new Member("social-id", "abc@example.com", "nickname", "01012345678", true); boolean result = member.isSameMobilePhoneNumber("01012345678"); @@ -166,7 +168,7 @@ class IsSameMobilePhoneNumberTest { @Test void ๋‹ค๋ฅธ_์ „ํ™”๋ฒˆํ˜ธ๋ฅผ_๋น„๊ตํ•˜๋ฉด_false๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { - Member member = new Member("social-id", "nickname", "01012345678", "๊ฐ•๋‚จ๊ตฌ", true); + Member member = new Member("social-id", "abc@example.com", "nickname", "01012345678", true); boolean result = member.isSameMobilePhoneNumber("01087654321"); diff --git a/src/test/java/timeeat/domain/member/MobilePhoneNumberTest.java b/src/test/java/eatda/domain/member/MobilePhoneNumberTest.java similarity index 94% rename from src/test/java/timeeat/domain/member/MobilePhoneNumberTest.java rename to src/test/java/eatda/domain/member/MobilePhoneNumberTest.java index aa5fbf7f..01525ca4 100644 --- a/src/test/java/timeeat/domain/member/MobilePhoneNumberTest.java +++ b/src/test/java/eatda/domain/member/MobilePhoneNumberTest.java @@ -1,15 +1,15 @@ -package timeeat.domain.member; +package eatda.domain.member; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; class MobilePhoneNumberTest { diff --git a/src/test/java/eatda/domain/store/CheerTest.java b/src/test/java/eatda/domain/store/CheerTest.java new file mode 100644 index 00000000..261ffd54 --- /dev/null +++ b/src/test/java/eatda/domain/store/CheerTest.java @@ -0,0 +1,60 @@ +package eatda.domain.store; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import eatda.domain.member.Member; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class CheerTest { + + private static final Member DEFAULT_MEMBER = new Member("socialId", "email@kakao.com", "nickname"); + private static final Store DEFAULT_STORE = Store.builder() + .kakaoId("1234567890") + .category(StoreCategory.CAFE) + .phoneNumber("02-1234-5678") + .name("Test Store") + .placeUrl("https://place.kakao.com/1234567890") + .roadAddress("์„œ์šธ์‹œ ์„ฑ๋ถ๊ตฌ ๋Œ€ํ•™๋กœ 1๊ธธ 1") + .lotNumberAddress("์„œ์šธ์‹œ ์„ฑ๋ถ๊ตฌ ๋™์„ ๋™ 1-1") + .latitude(37.5665) + .longitude(126.978) + .build(); + + @Nested + class Validate { + + @ParameterizedTest + @NullAndEmptySource + void ์„ค๋ช…์ด_๋น„์–ด์žˆ์œผ๋ฉด_์•ˆ๋œ๋‹ค(String description) { + BusinessException exception = assertThrows(BusinessException.class, () -> { + new Cheer(DEFAULT_MEMBER, DEFAULT_STORE, description, "imageKey"); + }); + + assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_CHEER_DESCRIPTION); + } + + @Test + void ์ด๋ฏธ์ง€_ํ‚ค๋Š”_null์ด_๊ฐ€๋Šฅํ•˜๋‹ค() { + assertThatCode(() -> new Cheer(DEFAULT_MEMBER, DEFAULT_STORE, "Great store!", null)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", "\t\n"}) + void ์ด๋ฏธ์ง€_ํ‚ค๋Š”_๋น„์–ด์žˆ์œผ๋ฉด_์•ˆ๋œ๋‹ค(String imageKey) { + BusinessException exception = assertThrows(BusinessException.class, () -> { + new Cheer(DEFAULT_MEMBER, DEFAULT_STORE, "Great store!", imageKey); + }); + + assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_CHEER_IMAGE_KEY); + } + } +} diff --git a/src/test/java/timeeat/domain/store/CoordinatesTest.java b/src/test/java/eatda/domain/store/CoordinatesTest.java similarity index 95% rename from src/test/java/timeeat/domain/store/CoordinatesTest.java rename to src/test/java/eatda/domain/store/CoordinatesTest.java index 092dd42b..8e7f84eb 100644 --- a/src/test/java/timeeat/domain/store/CoordinatesTest.java +++ b/src/test/java/eatda/domain/store/CoordinatesTest.java @@ -1,13 +1,13 @@ -package timeeat.domain.store; +package eatda.domain.store; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; class CoordinatesTest { diff --git a/src/test/java/timeeat/enums/StoreCategoryTest.java b/src/test/java/eatda/domain/store/StoreCategoryTest.java similarity index 89% rename from src/test/java/timeeat/enums/StoreCategoryTest.java rename to src/test/java/eatda/domain/store/StoreCategoryTest.java index ba6bbc4d..35cff0b2 100644 --- a/src/test/java/timeeat/enums/StoreCategoryTest.java +++ b/src/test/java/eatda/domain/store/StoreCategoryTest.java @@ -1,13 +1,13 @@ -package timeeat.enums; +package eatda.domain.store; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; class StoreCategoryTest { diff --git a/src/test/java/eatda/domain/store/StoreTest.java b/src/test/java/eatda/domain/store/StoreTest.java new file mode 100644 index 00000000..1fe2d6ae --- /dev/null +++ b/src/test/java/eatda/domain/store/StoreTest.java @@ -0,0 +1,50 @@ +package eatda.domain.store; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class StoreTest { + + private static final Store.StoreBuilder DEFAULT_BUILDER = Store.builder() + .kakaoId("123456789") + .category(StoreCategory.OTHER) + .phoneNumber("010-1234-5678") + .name("๊ฐ€๊ฒŒ ์ด๋ฆ„") + .placeUrl("https://place.kakao.com/123456789") + .roadAddress("") + .lotNumberAddress("์„œ์šธํŠน๋ณ„์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 123-45") + .latitude(37.5665) + .longitude(126.978); + + @Nested + class GetAddressDistrict { + + @Test + void ์ฃผ์†Œ_๊ตฌ_์ •๋ณด๋ฅผ_์ง€๋ฒˆ_์ฃผ์†Œ์—์„œ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + Store store = DEFAULT_BUILDER + .lotNumberAddress("์„œ์šธํŠน๋ณ„์‹œ ์„ฑ๋ถ๊ตฌ ์„๊ด€๋™ 123-45") + .build(); + + String actual = store.getAddressDistrict(); + + assertThat(actual).isEqualTo("์„ฑ๋ถ๊ตฌ"); + } + } + + @Nested + class GetAddressNeighborhood { + + @Test + void ์ฃผ์†Œ_๋™_์ •๋ณด๋ฅผ_์ง€๋ฒˆ_์ฃผ์†Œ์—์„œ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + Store store = DEFAULT_BUILDER + .lotNumberAddress("์„œ์šธํŠน๋ณ„์‹œ ์„ฑ๋ถ๊ตฌ ์„๊ด€๋™ 123-45") + .build(); + + String actual = store.getAddressNeighborhood(); + + assertThat(actual).isEqualTo("์„๊ด€๋™"); + } + } +} diff --git a/src/test/java/eatda/domain/story/StoryTest.java b/src/test/java/eatda/domain/story/StoryTest.java new file mode 100644 index 00000000..572f0601 --- /dev/null +++ b/src/test/java/eatda/domain/story/StoryTest.java @@ -0,0 +1,160 @@ +package eatda.domain.story; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import eatda.domain.member.Member; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class StoryTest { + + private static final Member MEMBER = Mockito.mock(Member.class); + + @Nested + class RegisterStory { + + @Test + void ์Šคํ† ๋ฆฌ๋ฅผ_์ •์ƒ์ ์œผ๋กœ_์ƒ์„ฑํ•œ๋‹ค() { + Story story = Story.builder() + .member(MEMBER) + .storeKakaoId("123") + .storeName("๊ณฑ์ฐฝ์ง‘") + .storeAddress("์„œ์šธ์‹œ ์„ฑ๋™๊ตฌ") + .storeCategory("ํ•œ์‹") + .description("์ •๋ง ๋ง›์žˆ์–ด์š”") + .imageKey("story/image.jpg") + .build(); + + assertThat(story.getStoreName()).isEqualTo("๊ณฑ์ฐฝ์ง‘"); + assertThat(story.getDescription()).isEqualTo("์ •๋ง ๋ง›์žˆ์–ด์š”"); + } + } + + @Nested + class ValidateMember { + + @Test + void ํšŒ์›์ด_null์ด๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + assertThatThrownBy(() -> + Story.builder() + .member(null) + .storeKakaoId("123") + .storeName("๊ณฑ์ฐฝ์ง‘") + .storeAddress("์„œ์šธ์‹œ ์„ฑ๋™๊ตฌ") + .storeCategory("ํ•œ์‹") + .description("์ •๋ง ๋ง›์žˆ์–ด์š”") + .imageKey("story/image.jpg") + .build() + ).isInstanceOf(BusinessException.class) + .hasMessage(BusinessErrorCode.STORY_MEMBER_REQUIRED.getMessage()); + } + } + + @Nested + class ValidateStore { + + @Test + void ๊ฐ€๊ฒŒ_ID๊ฐ€_๋น„์–ด์žˆ์œผ๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + assertThatThrownBy(() -> + Story.builder() + .member(MEMBER) + .storeKakaoId(" ") + .storeName("๊ณฑ์ฐฝ์ง‘") + .storeAddress("์„œ์šธ์‹œ") + .storeCategory("ํ•œ์‹") + .description("๋ง›์žˆ์Œ") + .imageKey("story/image.jpg") + .build() + ).isInstanceOf(BusinessException.class) + .hasMessage(BusinessErrorCode.INVALID_STORE_KAKAO_ID.getMessage()); + } + + @Test + void ๊ฐ€๊ฒŒ_์ด๋ฆ„์ด_๋น„์–ด์žˆ์œผ๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + assertThatThrownBy(() -> + Story.builder() + .member(MEMBER) + .storeKakaoId("123") + .storeName(" ") + .storeAddress("์„œ์šธ์‹œ") + .storeCategory("ํ•œ์‹") + .description("๋ง›์žˆ์Œ") + .imageKey("story/image.jpg") + .build() + ).isInstanceOf(BusinessException.class) + .hasMessage(BusinessErrorCode.INVALID_STORE_NAME.getMessage()); + } + + @Test + void ๊ฐ€๊ฒŒ_์ฃผ์†Œ๊ฐ€_๋น„์–ด์žˆ์œผ๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + assertThatThrownBy(() -> + Story.builder() + .member(MEMBER) + .storeKakaoId("123") + .storeName("๊ณฑ์ฐฝ์ง‘") + .storeAddress(" ") + .storeCategory("ํ•œ์‹") + .description("๋ง›์žˆ์Œ") + .imageKey("story/image.jpg") + .build() + ).isInstanceOf(BusinessException.class) + .hasMessage(BusinessErrorCode.INVALID_STORE_ADDRESS.getMessage()); + } + + @Test + void ๊ฐ€๊ฒŒ_์นดํ…Œ๊ณ ๋ฆฌ๊ฐ€_๋น„์–ด์žˆ์œผ๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + assertThatThrownBy(() -> + Story.builder() + .member(MEMBER) + .storeKakaoId("123") + .storeName("๊ณฑ์ฐฝ์ง‘") + .storeAddress("์„œ์šธ์‹œ") + .storeCategory(" ") + .description("๋ง›์žˆ์Œ") + .imageKey("story/image.jpg") + .build() + ).isInstanceOf(BusinessException.class) + .hasMessage(BusinessErrorCode.INVALID_STORE_CATEGORY.getMessage()); + } + } + + @Nested + class ValidateStory { + + @Test + void ์„ค๋ช…์ด_๋น„์–ด์žˆ์œผ๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + assertThatThrownBy(() -> + Story.builder() + .member(MEMBER) + .storeKakaoId("123") + .storeName("๊ณฑ์ฐฝ์ง‘") + .storeAddress("์„œ์šธ์‹œ") + .storeCategory("ํ•œ์‹") + .description(" ") + .imageKey("story/image.jpg") + .build() + ).isInstanceOf(BusinessException.class) + .hasMessage(BusinessErrorCode.INVALID_STORY_DESCRIPTION.getMessage()); + } + + @Test + void ์ด๋ฏธ์ง€๊ฐ€_๋น„์–ด์žˆ์œผ๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + assertThatThrownBy(() -> + Story.builder() + .member(MEMBER) + .storeKakaoId("123") + .storeName("๊ณฑ์ฐฝ์ง‘") + .storeAddress("์„œ์šธ์‹œ") + .storeCategory("ํ•œ์‹") + .description("๋ง›์žˆ์Œ") + .imageKey(" ") + .build() + ).isInstanceOf(BusinessException.class) + .hasMessage(BusinessErrorCode.INVALID_STORY_IMAGE_KEY.getMessage()); + } + } +} diff --git a/src/test/java/eatda/fixture/CheerGenerator.java b/src/test/java/eatda/fixture/CheerGenerator.java new file mode 100644 index 00000000..1a3af6a7 --- /dev/null +++ b/src/test/java/eatda/fixture/CheerGenerator.java @@ -0,0 +1,34 @@ +package eatda.fixture; + +import eatda.domain.member.Member; +import eatda.domain.store.Cheer; +import eatda.domain.store.Store; +import eatda.repository.store.CheerRepository; +import org.springframework.stereotype.Component; + +@Component +public class CheerGenerator { + + private static final String DEFAULT_IMAGE_KEY = "default-image-key"; + private static final String DEFAULT_DESCRIPTION = "์‘์›ํ•ฉ๋‹ˆ๋‹ค!"; + + private final CheerRepository cheerRepository; + + public CheerGenerator(CheerRepository cheerRepository) { + this.cheerRepository = cheerRepository; + } + + public Cheer generateAdmin(Member member, Store store) { + Cheer cheer = new Cheer(member, store, DEFAULT_DESCRIPTION, DEFAULT_IMAGE_KEY, true); + return cheerRepository.save(cheer); + } + + public Cheer generateCommon(Member member, Store store) { + return generateCommon(member, store, DEFAULT_IMAGE_KEY); + } + + public Cheer generateCommon(Member member, Store store, String imageKey) { + Cheer cheer = new Cheer(member, store, DEFAULT_DESCRIPTION, imageKey, false); + return cheerRepository.save(cheer); + } +} diff --git a/src/test/java/eatda/fixture/MemberGenerator.java b/src/test/java/eatda/fixture/MemberGenerator.java new file mode 100644 index 00000000..be21f321 --- /dev/null +++ b/src/test/java/eatda/fixture/MemberGenerator.java @@ -0,0 +1,38 @@ +package eatda.fixture; + +import eatda.domain.member.Member; +import eatda.repository.member.MemberRepository; +import org.springframework.stereotype.Component; + +@Component +public class MemberGenerator { + + private static final String DEFAULT_NICKNAME = "nickname"; + private static final String DEFAULT_EMAIL = "generatorEmail@example.com"; + + private final MemberRepository memberRepository; + + public MemberGenerator(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + public Member generate(String socialId) { + return memberRepository.save(new Member(socialId, DEFAULT_EMAIL, DEFAULT_NICKNAME)); + } + + public Member generateByEmail(String socialId, String email) { + return memberRepository.save(new Member(socialId, email, DEFAULT_NICKNAME)); + } + + public Member generateByNickname(String socialId, String nickname) { + return memberRepository.save(new Member(socialId, DEFAULT_EMAIL, nickname)); + } + + public Member generate(String socialId, String email, String nickname) { + return memberRepository.save(new Member(socialId, email, nickname)); + } + + public Member generateRegisteredMember(String nickname, String email, String socialId, String phoneNumber) { + return memberRepository.save(new Member(socialId, email, nickname, phoneNumber, true)); + } +} diff --git a/src/test/java/eatda/fixture/StoreGenerator.java b/src/test/java/eatda/fixture/StoreGenerator.java new file mode 100644 index 00000000..405d8c8a --- /dev/null +++ b/src/test/java/eatda/fixture/StoreGenerator.java @@ -0,0 +1,40 @@ +package eatda.fixture; + +import eatda.domain.store.Store; +import eatda.domain.store.StoreCategory; +import eatda.repository.store.StoreRepository; +import org.springframework.stereotype.Component; + +@Component +public class StoreGenerator { + + private static final StoreCategory DEFAULT_CATEGORY = StoreCategory.OTHER; + private static final String DEFAULT_PHONE_NUMBER = "010-1234-5678"; + private static final String DEFAULT_NAME = "๊ฐ€๊ฒŒ ์ด๋ฆ„"; + private static final String DEFAULT_PLACE_URL = "https://place.kakao.com/123456789"; + private static final String DEFAULT_ROAD_ADDRESS = ""; + + private static final double DEFAULT_LATITUDE = 37.5665; // Default latitude for Seoul + private static final double DEFAULT_LONGITUDE = 126.978; // Default longitude for Seoul + + private final StoreRepository storeRepository; + + public StoreGenerator(StoreRepository storeRepository) { + this.storeRepository = storeRepository; + } + + public Store generate(String kakaoId, String lotNumberAddress) { + Store store = Store.builder() + .kakaoId(kakaoId) + .category(DEFAULT_CATEGORY) + .phoneNumber(DEFAULT_PHONE_NUMBER) + .name(DEFAULT_NAME) + .placeUrl(DEFAULT_PLACE_URL) + .roadAddress(DEFAULT_ROAD_ADDRESS) + .lotNumberAddress(lotNumberAddress) + .latitude(DEFAULT_LATITUDE) + .longitude(DEFAULT_LONGITUDE) + .build(); + return storeRepository.save(store); + } +} diff --git a/src/test/java/eatda/repository/BaseRepositoryTest.java b/src/test/java/eatda/repository/BaseRepositoryTest.java new file mode 100644 index 00000000..2fd33200 --- /dev/null +++ b/src/test/java/eatda/repository/BaseRepositoryTest.java @@ -0,0 +1,38 @@ +package eatda.repository; + +import eatda.fixture.CheerGenerator; +import eatda.fixture.MemberGenerator; +import eatda.fixture.StoreGenerator; +import eatda.repository.member.MemberRepository; +import eatda.repository.store.CheerRepository; +import eatda.repository.store.StoreRepository; +import eatda.repository.story.StoryRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +@Import({MemberGenerator.class, StoreGenerator.class, CheerGenerator.class}) +@DataJpaTest +public abstract class BaseRepositoryTest { + + @Autowired + protected MemberGenerator memberGenerator; + + @Autowired + protected StoreGenerator storeGenerator; + + @Autowired + protected CheerGenerator cheerGenerator; + + @Autowired + protected MemberRepository memberRepository; + + @Autowired + protected StoreRepository storeRepository; + + @Autowired + protected CheerRepository cheerRepository; + + @Autowired + protected StoryRepository storyRepository; +} diff --git a/src/test/java/eatda/repository/DatabaseSchemaTest.java b/src/test/java/eatda/repository/DatabaseSchemaTest.java new file mode 100644 index 00000000..f6cee556 --- /dev/null +++ b/src/test/java/eatda/repository/DatabaseSchemaTest.java @@ -0,0 +1,67 @@ +package eatda.repository; + +import static org.assertj.core.api.Assertions.assertThatCode; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +class DatabaseSchemaTest { + + @Nested + @ActiveProfiles({"test", "flyway"}) + @SpringBootTest(webEnvironment = WebEnvironment.NONE) + @TestPropertySource(properties = { + "spring.datasource.url=jdbc:h2:mem:flyway:local;MODE=MySQL", + "spring.flyway.locations=classpath:db/migration,classpath:db/seed/local"}) + class LocalDatabaseSchemaTest { + + @Autowired + private Flyway flyway; + + @Test + void ๋กœ์ปฌ_๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค_์Šคํ‚ค๋งˆ๊ฐ€_์ •์ƒ์ ์œผ๋กœ_๋™์ž‘ํ•œ๋‹ค() { + assertThatCode(() -> flyway.migrate()).doesNotThrowAnyException(); + } + } + + @Nested + @ActiveProfiles({"test", "flyway"}) + @SpringBootTest(webEnvironment = WebEnvironment.NONE) + @TestPropertySource(properties = { + "spring.datasource.url=jdbc:h2:mem:flyway:dev;MODE=MySQL", + "spring.flyway.locations=classpath:db/migration,classpath:db/seed/dev"}) + class DevelopDatabaseSchemaTest { + + + @Autowired + private Flyway flyway; + + @Test + void ๊ฐœ๋ฐœ_๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค_์Šคํ‚ค๋งˆ๊ฐ€_์ •์ƒ์ ์œผ๋กœ_๋™์ž‘ํ•œ๋‹ค() { + assertThatCode(() -> flyway.migrate()).doesNotThrowAnyException(); + } + } + + @Nested + @ActiveProfiles({"test", "flyway"}) + @SpringBootTest(webEnvironment = WebEnvironment.NONE) + @TestPropertySource(properties = { + "spring.datasource.url=jdbc:h2:mem:flyway:prod;MODE=MySQL", + "spring.flyway.locations=classpath:db/migration,classpath:db/seed/prod"}) + class ProductionDatabaseSchemaTest { + + @Autowired + private Flyway flyway; + + @Test + void ์šด์˜_๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค_์Šคํ‚ค๋งˆ๊ฐ€_์ •์ƒ์ ์œผ๋กœ_๋™์ž‘ํ•œ๋‹ค() { + assertThatCode(() -> flyway.migrate()).doesNotThrowAnyException(); + } + } +} diff --git a/src/test/java/eatda/repository/store/CheerRepositoryTest.java b/src/test/java/eatda/repository/store/CheerRepositoryTest.java new file mode 100644 index 00000000..88b8ef31 --- /dev/null +++ b/src/test/java/eatda/repository/store/CheerRepositoryTest.java @@ -0,0 +1,44 @@ +package eatda.repository.store; + +import static org.assertj.core.api.Assertions.assertThat; + +import eatda.domain.member.Member; +import eatda.domain.store.Store; +import eatda.repository.BaseRepositoryTest; +import java.util.Optional; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class CheerRepositoryTest extends BaseRepositoryTest { + + @Nested + class FindRecentImageKey { + + @Test + void ์‘์›๋“ค_์ค‘_์ตœ๊ทผ_null์ด_์•„๋‹Œ_์ด๋ฏธ์ง€_ํ‚ค๋ฅผ_์กฐํšŒํ•œ๋‹ค() throws InterruptedException { + Member member = memberGenerator.generate("111"); + Store store = storeGenerator.generate("๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€", "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋Œ€์น˜๋™ 896-33"); + cheerGenerator.generateCommon(member, store, "image-key-1"); + Thread.sleep(5); + cheerGenerator.generateCommon(member, store, "image-key-2"); + cheerGenerator.generateCommon(member, store, null); + + Optional imageKey = cheerRepository.findRecentImageKey(store); + + assertThat(imageKey).contains("image-key-2"); + } + + @Test + void ์‘์›๋“ค์˜_์ด๋ฏธ์ง€๊ฐ€_๋ชจ๋‘_๋น„์–ด์žˆ๋‹ค๋ฉด_ํ•ด๋‹น_๊ฐ’์ด_์—†๋‹ค() { + Member member = memberGenerator.generate("111"); + Store store = storeGenerator.generate("๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€", "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋Œ€์น˜๋™ 896-33"); + cheerGenerator.generateCommon(member, store, null); + cheerGenerator.generateCommon(member, store, null); + cheerGenerator.generateCommon(member, store, null); + + Optional imageKey = cheerRepository.findRecentImageKey(store); + + assertThat(imageKey).isEmpty(); + } + } +} diff --git a/src/test/java/eatda/service/BaseServiceTest.java b/src/test/java/eatda/service/BaseServiceTest.java new file mode 100644 index 00000000..2eadb322 --- /dev/null +++ b/src/test/java/eatda/service/BaseServiceTest.java @@ -0,0 +1,65 @@ +package eatda.service; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; + +import eatda.DatabaseCleaner; +import eatda.client.map.MapClient; +import eatda.client.oauth.OauthClient; +import eatda.fixture.CheerGenerator; +import eatda.fixture.MemberGenerator; +import eatda.fixture.StoreGenerator; +import eatda.repository.member.MemberRepository; +import eatda.repository.store.CheerRepository; +import eatda.repository.store.StoreRepository; +import eatda.service.common.ImageService; +import org.junit.jupiter.api.BeforeEach; +import eatda.repository.story.StoryRepository; +import eatda.service.common.ImageService; +import eatda.service.store.StoreService; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +@ExtendWith(DatabaseCleaner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +public abstract class BaseServiceTest { + + private static final String MOCKED_IMAGE_KEY = "mocked-image-path"; + private static final String MOCKED_IMAGE_URL = "https://example.com/image.jpg"; + + @MockitoBean + protected OauthClient oauthClient; + + @MockitoBean + protected MapClient mapClient; + + @MockitoBean + protected ImageService imageService; + + @Autowired + protected MemberGenerator memberGenerator; + + @Autowired + protected StoreGenerator storeGenerator; + + @Autowired + protected CheerGenerator cheerGenerator; + + @Autowired + protected MemberRepository memberRepository; + + @Autowired + protected StoreRepository storeRepository; + + @Autowired + protected CheerRepository cheerRepository; + + @BeforeEach + void mockingImageService() { + doReturn(MOCKED_IMAGE_URL).when(imageService).getPresignedUrl(anyString()); + doReturn(MOCKED_IMAGE_KEY).when(imageService).upload(any(), any()); + } +} diff --git a/src/test/java/timeeat/service/auth/AuthServiceTest.java b/src/test/java/eatda/service/auth/AuthServiceTest.java similarity index 54% rename from src/test/java/timeeat/service/auth/AuthServiceTest.java rename to src/test/java/eatda/service/auth/AuthServiceTest.java index c2d132bf..68089a84 100644 --- a/src/test/java/timeeat/service/auth/AuthServiceTest.java +++ b/src/test/java/eatda/service/auth/AuthServiceTest.java @@ -1,20 +1,38 @@ -package timeeat.service.auth; +package eatda.service.auth; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; - +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; + +import eatda.client.oauth.OauthMemberInformation; +import eatda.client.oauth.OauthToken; +import eatda.controller.auth.LoginRequest; +import eatda.controller.member.MemberResponse; +import eatda.service.BaseServiceTest; +import java.net.URI; +import java.net.URISyntaxException; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import timeeat.controller.auth.LoginRequest; -import timeeat.controller.member.MemberResponse; -import timeeat.service.BaseServiceTest; class AuthServiceTest extends BaseServiceTest { + private static final OauthToken DEFAULT_OAUTH_TOKEN = new OauthToken("oauth-access-token"); + private static final OauthMemberInformation DEFAULT_OAUTH_MEMBER_INFO = + new OauthMemberInformation(123L, "authService@kakao.com", "nickname"); + @Autowired private AuthService authService; + @BeforeEach + protected final void mockingClient() throws URISyntaxException { + doReturn(new URI("http://localhost:8080/login/callback")).when(oauthClient).getOauthLoginUrl(anyString()); + doReturn(DEFAULT_OAUTH_TOKEN).when(oauthClient).requestOauthToken(anyString(), anyString()); + doReturn(DEFAULT_OAUTH_MEMBER_INFO).when(oauthClient).requestMemberInformation(DEFAULT_OAUTH_TOKEN); + } + @Nested class Login { @@ -28,7 +46,6 @@ class Login { () -> assertThat(response.isSignUp()).isTrue(), () -> assertThat(response.nickname()).isNotNull(), () -> assertThat(response.phoneNumber()).isNull(), - () -> assertThat(response.interestArea()).isNull(), () -> assertThat(response.optInMarketing()).isNull() ); } diff --git a/src/test/java/eatda/service/common/ImageServiceTest.java b/src/test/java/eatda/service/common/ImageServiceTest.java new file mode 100644 index 00000000..ec551d27 --- /dev/null +++ b/src/test/java/eatda/service/common/ImageServiceTest.java @@ -0,0 +1,135 @@ +package eatda.service.common; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; +import java.net.URL; +import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; +import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest; + +@ExtendWith(MockitoExtension.class) +class ImageServiceTest { + + private static final String TEST_BUCKET = "test-bucket"; + + @Mock + private S3Client s3Client; + + @Mock + private S3Presigner s3Presigner; + private ImageService imageService; + + @BeforeEach + void setUp() { + imageService = new ImageService(s3Client, TEST_BUCKET, s3Presigner); + } + + @Nested + class FileUpload { + + @ParameterizedTest + @EnumSource(ImageDomain.class) + void ํ—ˆ์šฉ๋œ_์ด๋ฏธ์ง€_ํƒ€์ž…์ด๋ฉด_์ •์ƒ์ ์œผ๋กœ_์—…๋กœ๋“œ๋˜๊ณ _์ƒ์„ฑ๋œ_Key๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค(ImageDomain imageDomain) { + String originalFilename = "test-image.jpg"; + String contentType = "image/jpeg"; + + MockMultipartFile file = new MockMultipartFile( + "image", originalFilename, contentType, "image-content".getBytes() + ); + + String key = imageService.upload(file, imageDomain); + + ArgumentCaptor putObjectRequestCaptor = ArgumentCaptor.forClass(PutObjectRequest.class); + verify(s3Client).putObject(putObjectRequestCaptor.capture(), any(RequestBody.class)); + PutObjectRequest capturedRequest = putObjectRequestCaptor.getValue(); + + String expectedPattern = imageDomain.getName() + "/[a-f0-9\\-]{36}\\.jpg"; + + assertAll( + () -> assertThat(key).matches(expectedPattern), + () -> assertThat(capturedRequest.key()).isEqualTo(key), + () -> assertThat(capturedRequest.bucket()).isEqualTo(TEST_BUCKET), + () -> assertThat(capturedRequest.contentType()).isEqualTo(contentType) + ); + } + + @Test + void ํ—ˆ์šฉ๋˜์ง€_์•Š์€_ํŒŒ์ผ_ํƒ€์ž…์ด๋ฉด_BusinessException์„_๋˜์ง„๋‹ค() { + MockMultipartFile file = new MockMultipartFile( + "file", "test.txt", "text/plain", "file-content".getBytes() + ); + + BusinessException exception = assertThrows(BusinessException.class, + () -> imageService.upload(file, ImageDomain.STORY)); + + assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_IMAGE_TYPE); + } + } + + @Nested + class GeneratePresignedUrl { + + @Test + void ์œ ํšจํ•œ_key๋กœ_์š”์ฒญ_์‹œ_Presigned_URL์„_์„ฑ๊ณต์ ์œผ๋กœ_๋ฐ˜ํ™˜ํ•œ๋‹ค() throws Exception { + String key = "stores/image.jpg"; + String expectedUrlString = "https://example.com/presigned-url-for-image.jpg"; + URL expectedUrl = new URL(expectedUrlString); + + PresignedGetObjectRequest presignedRequestResult = mock(PresignedGetObjectRequest.class); + + when(presignedRequestResult.url()).thenReturn(expectedUrl); + when(s3Presigner.presignGetObject(any(GetObjectPresignRequest.class))) + .thenReturn(presignedRequestResult); + + String presignedUrl = imageService.getPresignedUrl(key); + + ArgumentCaptor presignRequestCaptor = + ArgumentCaptor.forClass(GetObjectPresignRequest.class); + verify(s3Presigner).presignGetObject(presignRequestCaptor.capture()); + GetObjectPresignRequest capturedPresignRequest = presignRequestCaptor.getValue(); + + assertAll( + () -> assertThat(presignedUrl).isEqualTo(expectedUrlString), + () -> assertThat(capturedPresignRequest.getObjectRequest().key()).isEqualTo(key), + () -> assertThat(capturedPresignRequest.getObjectRequest().bucket()).isEqualTo(TEST_BUCKET), + () -> assertThat(capturedPresignRequest.signatureDuration()).isEqualTo(Duration.ofMinutes(30)) + ); + } + + @Test + void Presigner๊ฐ€_์˜ˆ์™ธ๋ฅผ_๋˜์ง€๋ฉด_BusinessException์œผ๋กœ_์ „ํ™˜ํ•˜์—ฌ_๋˜์ง„๋‹ค() { + String key = "stores/image.jpg"; + + when(s3Presigner.presignGetObject(any(GetObjectPresignRequest.class))) + .thenThrow(SdkClientException.create("AWS SDK ํ†ต์‹  ์‹คํŒจ")); + + BusinessException exception = assertThrows(BusinessException.class, + () -> imageService.getPresignedUrl(key)); + + assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.PRESIGNED_URL_GENERATION_FAILED); + } + } +} diff --git a/src/test/java/timeeat/service/service/MemberServiceTest.java b/src/test/java/eatda/service/member/MemberServiceTest.java similarity index 66% rename from src/test/java/timeeat/service/service/MemberServiceTest.java rename to src/test/java/eatda/service/member/MemberServiceTest.java index 1c01be9e..f94da0c9 100644 --- a/src/test/java/timeeat/service/service/MemberServiceTest.java +++ b/src/test/java/eatda/service/member/MemberServiceTest.java @@ -1,32 +1,61 @@ -package timeeat.service.service; +package eatda.service.member; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertThrows; +import eatda.controller.member.MemberResponse; +import eatda.controller.member.MemberUpdateRequest; +import eatda.domain.member.Member; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; +import eatda.service.BaseServiceTest; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import timeeat.controller.member.MemberResponse; -import timeeat.controller.member.MemberUpdateRequest; -import timeeat.domain.member.Member; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; -import timeeat.service.BaseServiceTest; class MemberServiceTest extends BaseServiceTest { @Autowired private MemberService memberService; + @Nested + class GetMember { + + @Test + void ํšŒ์›_์ •๋ณด๋ฅผ_์กฐํšŒํ• _์ˆ˜_์žˆ๋‹ค() { + Member member = memberGenerator.generateRegisteredMember("123", "abc@kakao.com", "nickname", "01012345678"); + + MemberResponse response = memberService.getMember(member.getId()); + + assertAll( + () -> assertThat(response.id()).isEqualTo(member.getId()), + () -> assertThat(response.nickname()).isEqualTo(member.getNickname()), + () -> assertThat(response.phoneNumber()).isEqualTo(member.getPhoneNumber()), + () -> assertThat(response.optInMarketing()).isEqualTo(member.isOptInMarketing()), + () -> assertThat(response.isSignUp()).isFalse() + ); + } + + @Test + void ์กด์žฌํ•˜์ง€_์•Š๋Š”_ํšŒ์›์˜_์ •๋ณด๋ฅผ_์กฐํšŒํ•˜๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + long nonExistentMemberId = 999L; + + BusinessException exception = assertThrows(BusinessException.class, + () -> memberService.getMember(nonExistentMemberId)); + + assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_MEMBER_ID); + } + } + @Nested class ValidateNickname { @Test void ์ค‘๋ณต๋˜์ง€_์•Š์€_๋‹‰๋„ค์ž„์ด๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•˜์ง€_์•Š๋Š”๋‹ค() { - memberGenerator.generate("123", "nickname"); - Member member = memberGenerator.generate("456", "unique-nickname"); + memberGenerator.generate("123", "abc@kakao.com", "nickname"); + Member member = memberGenerator.generate("456", "def@kakao.com", "unique-nickname"); String newNickname = "new-unique-nickname"; assertThatCode(() -> memberService.validateNickname(newNickname, member.getId())) @@ -35,7 +64,7 @@ class ValidateNickname { @Test void ์ž์‹ ์˜_๊ธฐ์กด_๋‹‰๋„ค์ž„์ด๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•˜์ง€_์•Š๋Š”๋‹ค() { - Member member = memberGenerator.generate("123", "nickname"); + Member member = memberGenerator.generateByNickname("123", "nickname"); String newNickname = "nickname"; assertThatCode(() -> memberService.validateNickname(newNickname, member.getId())) @@ -44,8 +73,8 @@ class ValidateNickname { @Test void ์ค‘๋ณต๋œ_๋‹‰๋„ค์ž„์ด_์žˆ์œผ๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { - memberGenerator.generate("123", "duplicate-nickname"); - Member member = memberGenerator.generate("456", "another-nickname"); + memberGenerator.generate("123", "abc@kakao.com", "duplicate-nickname"); + Member member = memberGenerator.generate("456", "def@kakao.com", "another-nickname"); String newNickname = "duplicate-nickname"; BusinessException exception = assertThrows(BusinessException.class, @@ -60,8 +89,8 @@ class ValidatePhoneNumber { @Test void ์ค‘๋ณต๋˜์ง€_์•Š์€_์ „ํ™”๋ฒˆํ˜ธ์ด๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•˜์ง€_์•Š๋Š”๋‹ค() { - memberGenerator.generate("123", "nickname"); - Member member = memberGenerator.generate("456", "unique-nickname"); + memberGenerator.generate("123", "abc@kakao.com", "nickname"); + Member member = memberGenerator.generate("456", "def@kakao.com", "unique-nickname"); String newPhoneNumber = "01012345678"; assertThatCode(() -> memberService.validatePhoneNumber(newPhoneNumber, member.getId())) @@ -70,7 +99,7 @@ class ValidatePhoneNumber { @Test void ์ž์‹ ์˜_๊ธฐ์กด_์ „ํ™”๋ฒˆํ˜ธ์ด๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•˜์ง€_์•Š๋Š”๋‹ค() { - Member member = memberGenerator.generateRegisteredMember("123", "nickname", "01012345678"); + Member member = memberGenerator.generateRegisteredMember("nickname", "hij@kakao.com", "123", "01012345678"); String newPhoneNumber = "01012345678"; assertThatCode(() -> memberService.validatePhoneNumber(newPhoneNumber, member.getId())) @@ -79,8 +108,9 @@ class ValidatePhoneNumber { @Test void ์ค‘๋ณต๋œ_์ „ํ™”๋ฒˆํ˜ธ๊ฐ€_์žˆ์œผ๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { - memberGenerator.generateRegisteredMember("123", "nickname1", "01012345678"); - Member member = memberGenerator.generateRegisteredMember("456", "nickname2", "01087654321"); + memberGenerator.generateRegisteredMember("nickname1", "abc@kakao.com", "123", "01012345678"); + Member member = memberGenerator.generateRegisteredMember("nickname2", "def@kakao.com", "456", + "01087654321"); String newPhoneNumber = "01012345678"; BusinessException exception = assertThrows(BusinessException.class, @@ -96,7 +126,7 @@ class Update { @Test void ํšŒ์›_์ •๋ณด๋ฅผ_์ˆ˜์ •ํ• _์ˆ˜_์žˆ๋‹ค() { Member member = memberGenerator.generate("123"); - MemberUpdateRequest request = new MemberUpdateRequest("update-nickname", "01012345678", "์„ฑ๋ถ๊ตฌ", true); + MemberUpdateRequest request = new MemberUpdateRequest("update-nickname", "01012345678", true); MemberResponse response = memberService.update(member.getId(), request); @@ -105,17 +135,16 @@ class Update { () -> assertThat(response.isSignUp()).isFalse(), () -> assertThat(response.nickname()).isEqualTo("update-nickname"), () -> assertThat(response.phoneNumber()).isEqualTo("01012345678"), - () -> assertThat(response.interestArea()).isEqualTo("์„ฑ๋ถ๊ตฌ"), () -> assertThat(response.optInMarketing()).isTrue() ); } @Test void ์ค‘๋ณต๋œ_๋‹‰๋„ค์ž„์ด_์žˆ์œผ๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { - Member existMember = memberGenerator.generate("123", "duplicate-nickname"); - Member updatedMember = memberGenerator.generate("456"); + Member existMember = memberGenerator.generate("123", "abc@kakao.com", "duplicate-nickname"); + Member updatedMember = memberGenerator.generate("456", "def@kakao.com", "another-nickname"); MemberUpdateRequest request = - new MemberUpdateRequest(existMember.getNickname(), "01012345678", "์„ฑ๋ถ๊ตฌ", true); + new MemberUpdateRequest(existMember.getNickname(), "01012345678", true); BusinessException exception = assertThrows(BusinessException.class, () -> memberService.update(updatedMember.getId(), request)); @@ -125,9 +154,9 @@ class Update { @Test void ๊ธฐ์กด์˜_๋‹‰๋„ค์ž„๊ณผ_๋™์ผํ•˜๋ฉด_์žˆ์œผ๋ฉด_์ •์ƒ์ ์œผ๋กœ_ํšŒ์›_์ •๋ณด๊ฐ€_์ˆ˜์ •๋œ๋‹ค() { - Member member = memberGenerator.generate("123", "duplicate-nickname"); + Member member = memberGenerator.generateByNickname("123", "duplicate-nickname"); MemberUpdateRequest request = - new MemberUpdateRequest(member.getNickname(), "01012345678", "์„ฑ๋ถ๊ตฌ", true); + new MemberUpdateRequest(member.getNickname(), "01012345678", true); MemberResponse response = memberService.update(member.getId(), request); @@ -137,10 +166,10 @@ class Update { @Test void ์ค‘๋ณต๋œ_์ „ํ™”๋ฒˆํ˜ธ๊ฐ€_์žˆ์œผ๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { String phoneNumber = "01012345678"; - memberGenerator.generateRegisteredMember("123", "nickname1", phoneNumber); - Member updatedMember = memberGenerator.generate("456", "nickname2"); + memberGenerator.generateRegisteredMember("nickname1", "hij@kakao.com", "123", phoneNumber); + Member updatedMember = memberGenerator.generate("456", "abc@kakao.com", "nickname2"); MemberUpdateRequest request = - new MemberUpdateRequest("new-nickname", phoneNumber, "์„ฑ๋ถ๊ตฌ", true); + new MemberUpdateRequest("new-nickname", phoneNumber, true); BusinessException exception = assertThrows(BusinessException.class, () -> memberService.update(updatedMember.getId(), request)); @@ -151,9 +180,9 @@ class Update { @Test void ๊ธฐ์กด์˜_์ „ํ™”๋ฒˆํ˜ธ์™€_๋™์ผํ•˜๋ฉด_์ •์ƒ์ ์œผ๋กœ_ํšŒ์›_์ •๋ณด๊ฐ€_์ˆ˜์ •๋œ๋‹ค() { String phoneNumber = "01012345678"; - Member member = memberGenerator.generateRegisteredMember("123", "nickname1", phoneNumber); + Member member = memberGenerator.generateRegisteredMember("nickname1", "hij@kakao.com", "123", phoneNumber); MemberUpdateRequest request = - new MemberUpdateRequest("new-nickname", phoneNumber, "์„ฑ๋ถ๊ตฌ", true); + new MemberUpdateRequest("new-nickname", phoneNumber, true); MemberResponse response = memberService.update(member.getId(), request); diff --git a/src/test/java/eatda/service/store/CheerServiceTest.java b/src/test/java/eatda/service/store/CheerServiceTest.java new file mode 100644 index 00000000..184b077b --- /dev/null +++ b/src/test/java/eatda/service/store/CheerServiceTest.java @@ -0,0 +1,41 @@ +package eatda.service.store; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import eatda.controller.store.CheersResponse; +import eatda.domain.member.Member; +import eatda.domain.store.Cheer; +import eatda.domain.store.Store; +import eatda.service.BaseServiceTest; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class CheerServiceTest extends BaseServiceTest { + + @Autowired + private CheerService cheerService; + + @Nested + class GetCheers { + + @Test + void ์š”์ฒญํ•œ_์‘์›_๊ฐœ์ˆ˜๋งŒํผ_์‘์›์„_์ตœ์‹ ์ˆœ์œผ๋กœ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + Member member = memberGenerator.generate("123"); + Store store1 = storeGenerator.generate("123", "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 123-45"); + Store store2 = storeGenerator.generate("456", "์„œ์šธ์‹œ ์„ฑ๋ถ๊ตฌ ์„๊ด€๋™ 123-45"); + Cheer cheer1 = cheerGenerator.generateAdmin(member, store1); + Cheer cheer2 = cheerGenerator.generateAdmin(member, store1); + Cheer cheer3 = cheerGenerator.generateAdmin(member, store2); + + CheersResponse response = cheerService.getCheers(2); + + assertAll( + () -> assertThat(response.cheers()).hasSize(2), + () -> assertThat(response.cheers().get(0).cheerId()).isEqualTo(cheer3.getId()), + () -> assertThat(response.cheers().get(1).cheerId()).isEqualTo(cheer2.getId()) + ); + } + } +} diff --git a/src/test/java/eatda/service/store/StoreSearchFilterTest.java b/src/test/java/eatda/service/store/StoreSearchFilterTest.java new file mode 100644 index 00000000..d6eca025 --- /dev/null +++ b/src/test/java/eatda/service/store/StoreSearchFilterTest.java @@ -0,0 +1,42 @@ +package eatda.service.store; + +import static org.assertj.core.api.Assertions.assertThat; + +import eatda.client.map.StoreSearchResult; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class StoreSearchFilterTest { + + private final StoreSearchFilter storeSearchFilter = new StoreSearchFilter(); + + @Nested + class FilterSearchedStores { + + @Test + void ๋นˆ_๊ฒ€์ƒ‰_๊ฒฐ๊ณผ๋ฅผ_๋„ฃ์œผ๋ฉด_๋นˆ_๋ฆฌ์ŠคํŠธ๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + List actual = storeSearchFilter.filterSearchedStores(List.of()); + + assertThat(actual).isEmpty(); + } + + @Test + void ์Œ์‹์ ์ด_์•„๋‹ˆ๊ฑฐ๋‚˜_์„œ์šธ์—_์œ„์น˜ํ•˜์ง€_์•Š๋Š”_๊ฐ€๊ฒŒ๋Š”_์ œ์™ธํ•œ๋‹ค() { + StoreSearchResult store1 = createStore("1", "์„œ์šธ์Œ์‹์ 1","FD6", "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋Œ€์น˜๋™ 896-33"); + StoreSearchResult store2 = createStore("2", "์นดํŽ˜", "CD2","์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋Œ€์น˜๋™ 896-33"); + StoreSearchResult store3 = createStore("3", "์„œ์šธ์Œ์‹์ 2", "FD6","์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋Œ€์น˜๋™ 896-33"); + StoreSearchResult store4 = createStore("4", "๋ถ€์‚ฐ์Œ์‹์ ", "FD6","๋ถ€์‚ฐ ์—ฐ์ œ๊ตฌ ์—ฐ์‚ฐ๋™ 632-8"); + + List searchResults = List.of(store1, store2, store3, store4); + List actual = storeSearchFilter.filterSearchedStores(searchResults); + + assertThat(actual).containsExactly(store1, store3); + } + + private StoreSearchResult createStore(String id, String name, String categoryGroupCode, String location) { + return new StoreSearchResult(id, categoryGroupCode, "์Œ์‹์  > ์‹๋‹น", "010-1234-1234", name, "https://yapp.co.kr", + location, null, 37.0d, 128.0d); + } + } +} diff --git a/src/test/java/eatda/service/store/StoreServiceTest.java b/src/test/java/eatda/service/store/StoreServiceTest.java new file mode 100644 index 00000000..b36b95bb --- /dev/null +++ b/src/test/java/eatda/service/store/StoreServiceTest.java @@ -0,0 +1,77 @@ +package eatda.service.store; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; + +import eatda.client.map.StoreSearchResult; +import eatda.domain.member.Member; +import eatda.domain.store.Store; +import eatda.service.BaseServiceTest; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class StoreServiceTest extends BaseServiceTest { + + @Autowired + private StoreService storeService; + + @Nested + class GetStores { + + @Test + void ์Œ์‹์ _๋ชฉ๋ก์„_์ตœ์‹ ์ˆœ์œผ๋กœ_์กฐํšŒํ•œ๋‹ค() { + Member member = memberGenerator.generate("111"); + Store store1 = storeGenerator.generate("๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€", "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋Œ€์น˜๋™ 896-33"); + cheerGenerator.generateCommon(member, store1, "image-key-1"); + Store store2 = storeGenerator.generate("์„๊ด€๋™๋–ก๋ณถ์ด", "์„œ์šธ ์„ฑ๋ถ๊ตฌ ์„๊ด€๋™ 123-45"); + cheerGenerator.generateCommon(member, store2, "image-key-2"); + Store store3 = storeGenerator.generate("๊ฐ•๋‚จ์ˆœ๋Œ€๊ตญ", "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 678-90"); + cheerGenerator.generateCommon(member, store3, "image-key-3"); + + int size = 2; + + var response = storeService.getStores(size); + + assertAll( + () -> assertThat(response.stores()).hasSize(size), + () -> assertThat(response.stores().get(0).id()).isEqualTo(store3.getId()), + () -> assertThat(response.stores().get(1).id()).isEqualTo(store2.getId()) + ); + } + } + + @Nested + class SearchStores { + + @Test + void ์Œ์‹์ _๊ฒ€์ƒ‰_๊ฒฐ๊ณผ๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + mockingMapClient(); + String query = "๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€"; + + var response = storeService.searchStores(query); + + assertAll( + () -> assertThat(response.stores()).hasSize(2), + () -> assertThat(response.stores().get(0).kakaoId()).isEqualTo("123"), + () -> assertThat(response.stores().get(0).address()).isEqualTo("์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋Œ€์น˜๋™ 896-33"), + () -> assertThat(response.stores().get(1).kakaoId()).isEqualTo("456"), + () -> assertThat(response.stores().get(1).address()).isEqualTo("์„œ์šธ ์ค‘๊ตฌ ๋ถ์ฐฝ๋™ 19-4") + ); + } + } + + void mockingMapClient() { + List searchResults = List.of( + new StoreSearchResult("123", "FD6", "์Œ์‹์  > ํ•œ์‹ > ๊ตญ๋ฐฅ", "010-1234-1234", "๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€ ๋ณธ์ ", "https://yapp.co.kr", + "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋Œ€์น˜๋™ 896-33", "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ์„ ๋ฆ‰๋กœ86๊ธธ 40-4", 37.0d, 128.0d), + new StoreSearchResult("456", "FD6", "์Œ์‹์  > ํ•œ์‹ > ๊ตญ๋ฐฅ", "010-1234-1234", "๋†๋ฏผ๋ฐฑ์•”์ˆœ๋Œ€ ์‹œ์ฒญ์ ", "http://yapp.kr", + "์„œ์šธ ์ค‘๊ตฌ ๋ถ์ฐฝ๋™ 19-4", null, 37.0d, 128.0d) + ); + + doReturn(searchResults).when(mapClient).searchShops(anyString()); + } +} diff --git a/src/test/java/eatda/service/story/StoryServiceTest.java b/src/test/java/eatda/service/story/StoryServiceTest.java new file mode 100644 index 00000000..ce56540b --- /dev/null +++ b/src/test/java/eatda/service/story/StoryServiceTest.java @@ -0,0 +1,61 @@ +package eatda.service.story; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import eatda.client.map.StoreSearchResult; +import eatda.controller.story.StoryRegisterRequest; +import eatda.domain.member.Member; +import eatda.exception.BusinessErrorCode; +import eatda.exception.BusinessException; +import eatda.service.BaseServiceTest; +import eatda.service.common.ImageDomain; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.multipart.MultipartFile; + +public class StoryServiceTest extends BaseServiceTest { + + @Autowired + private StoryService storyService; + + @Nested + class RegisterStory { + + @Test + void ์Šคํ† ๋ฆฌ_๋“ฑ๋ก์—_์„ฑ๊ณตํ•œ๋‹ค() { + Member member = memberGenerator.generate("12345"); + StoryRegisterRequest request = new StoryRegisterRequest("๊ณฑ์ฐฝ", "123", "๋ฏธ์ณค๋‹ค ์—ฌ๊ธฐ"); + MultipartFile image = mock(MultipartFile.class); + + StoreSearchResult store = new StoreSearchResult( + "123", "FD6", "์Œ์‹์  > ํ•œ์‹", "010-1234-5678", + "๊ณฑ์ฐฝ์ง‘", "http://example.com", + "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ", "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ", 37.0, 127.0 + ); + doReturn(List.of(store)).when(mapClient).searchShops(request.query()); + when(imageService.upload(image, ImageDomain.STORY)).thenReturn("image-key"); + + assertDoesNotThrow(() -> storyService.registerStory(request, image, member.getId())); + } + + @Test + void ํด๋ผ์ด์–ธํŠธ_์š”์ฒญ๊ณผ_์ผ์น˜ํ•˜๋Š”_๊ฐ€๊ฒŒ๊ฐ€_์—†์œผ๋ฉด_์‹คํŒจํ•œ๋‹ค() { + Member member = memberGenerator.generate("12345"); + StoryRegisterRequest request = new StoryRegisterRequest("๊ณฑ์ฐฝ", "999", "๋ฏธ์ณค๋‹ค ์—ฌ๊ธฐ"); + + MultipartFile image = mock(MultipartFile.class); + doReturn(Collections.emptyList()).when(mapClient).searchShops(request.query()); + + assertThatThrownBy(() -> storyService.registerStory(request, image, member.getId())) + .isInstanceOf(BusinessException.class) + .hasMessageContaining(BusinessErrorCode.STORE_NOT_FOUND.getMessage()); + } + } +} diff --git a/src/test/java/timeeat/controller/BaseControllerTest.java b/src/test/java/timeeat/controller/BaseControllerTest.java deleted file mode 100644 index 67378ca5..00000000 --- a/src/test/java/timeeat/controller/BaseControllerTest.java +++ /dev/null @@ -1,89 +0,0 @@ -package timeeat.controller; - -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doReturn; - -import io.restassured.RestAssured; -import io.restassured.builder.RequestSpecBuilder; -import io.restassured.filter.Filter; -import io.restassured.filter.log.RequestLoggingFilter; -import io.restassured.filter.log.ResponseLoggingFilter; -import io.restassured.specification.RequestSpecification; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.test.context.bean.override.mockito.MockitoBean; -import timeeat.DatabaseCleaner; -import timeeat.client.oauth.OauthClient; -import timeeat.client.oauth.OauthMemberInformation; -import timeeat.client.oauth.OauthToken; -import timeeat.controller.web.jwt.JwtManager; -import timeeat.domain.member.Member; -import timeeat.fixture.MemberGenerator; -import timeeat.repository.member.MemberRepository; - -@ExtendWith(DatabaseCleaner.class) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class BaseControllerTest { - - private static final List SPEC_FILTERS = List.of(new RequestLoggingFilter(), new ResponseLoggingFilter()); - - private static final OauthToken DEFAULT_OAUTH_TOKEN = new OauthToken("oauth-access-token"); - private static final OauthMemberInformation DEFAULT_OAUTH_MEMBER_INFO = - new OauthMemberInformation(314159248183772L, "nickname"); - - @LocalServerPort - private int port; - - @Autowired - protected MemberGenerator memberGenerator; - - @Autowired - protected MemberRepository memberRepository; - - @Autowired - private JwtManager jwtManager; - - @MockitoBean - private OauthClient oauthClient; - - private RequestSpecification spec; - - @BeforeEach - final void setEnvironment() { - RestAssured.port = port; - spec = new RequestSpecBuilder() - .addFilters(SPEC_FILTERS) - .build(); - } - - @BeforeEach - final void mockingClient() throws URISyntaxException { - doReturn(new URI("http://localhost:8080/login/callback")).when(oauthClient).getOauthLoginUrl(anyString()); - doReturn(DEFAULT_OAUTH_TOKEN).when(oauthClient).requestOauthToken(anyString(), anyString()); - doReturn(DEFAULT_OAUTH_MEMBER_INFO).when(oauthClient).requestMemberInformation(DEFAULT_OAUTH_TOKEN); - } - - protected final RequestSpecification given() { - return RestAssured.given(spec); - } - - protected final String accessToken() { - Member member = memberGenerator.generate(Long.toString(DEFAULT_OAUTH_MEMBER_INFO.socialId())); - return jwtManager.issueAccessToken(member.getId()); - } - - protected final String refreshToken() { - Member member = memberGenerator.generate(Long.toString(DEFAULT_OAUTH_MEMBER_INFO.socialId())); - return jwtManager.issueRefreshToken(member.getId()); - } - - protected final String oauthLoginSocialId() { - return Long.toString(DEFAULT_OAUTH_MEMBER_INFO.socialId()); - } -} diff --git a/src/test/java/timeeat/document/member/MemberDocumentTest.java b/src/test/java/timeeat/document/member/MemberDocumentTest.java deleted file mode 100644 index 36857092..00000000 --- a/src/test/java/timeeat/document/member/MemberDocumentTest.java +++ /dev/null @@ -1,130 +0,0 @@ -package timeeat.document.member; - -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.payload.JsonFieldType.BOOLEAN; -import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; -import static org.springframework.restdocs.payload.JsonFieldType.STRING; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; - -import io.restassured.http.ContentType; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.http.HttpHeaders; -import timeeat.controller.member.MemberResponse; -import timeeat.controller.member.MemberUpdateRequest; -import timeeat.document.BaseDocumentTest; -import timeeat.document.RestDocsRequest; -import timeeat.document.RestDocsResponse; -import timeeat.document.Tag; - -public class MemberDocumentTest extends BaseDocumentTest { - - @Nested - class CheckNickname { - - RestDocsRequest requestDocument = request() - .tag(Tag.MEMBER_API) - .summary("๋‹‰๋„ค์ž„ ์ค‘๋ณต ๊ฒ€์‚ฌ") - .requestHeader( - headerWithName(HttpHeaders.AUTHORIZATION).description("์•ก์„ธ์Šค ํ† ํฐ") - ).queryParameter( - parameterWithName("nickname").description("๊ฒ€์‚ฌํ•  ๋‹‰๋„ค์ž„") - ); - - @Test - void ์ค‘๋ณต๋˜์ง€_์•Š๋Š”_๋‹‰๋„ค์ž„์„_ํ™•์ธํ• _์ˆ˜_์žˆ๋‹ค() { - doNothing().when(memberService).validateNickname(anyString(), anyLong()); - - var document = document("member/nickname-check", 204) - .request(requestDocument) - .build(); - - given(document) - .contentType(ContentType.JSON) - .header(HttpHeaders.AUTHORIZATION, accessToken()) - .queryParam("nickname", "new-nickname") - .when().get("/api/member/nickname/check") - .then().statusCode(204); - } - } - - @Nested - class CheckPhoneNumber { - - RestDocsRequest requestDocument = request() - .tag(Tag.MEMBER_API) - .summary("์ „ํ™”๋ฒˆํ˜ธ ์ค‘๋ณต ๊ฒ€์‚ฌ") - .requestHeader( - headerWithName(HttpHeaders.AUTHORIZATION).description("์•ก์„ธ์Šค ํ† ํฐ") - ).queryParameter( - parameterWithName("phoneNumber").description("๊ฒ€์‚ฌํ•  ์ „ํ™”๋ฒˆํ˜ธ ex) 01012345678") - ); - - @Test - void ์ค‘๋ณต๋˜์ง€_์•Š๋Š”_์ „ํ™”๋ฒˆํ˜ธ๋ฅผ_ํ™•์ธํ• _์ˆ˜_์žˆ๋‹ค() { - doNothing().when(memberService).validatePhoneNumber(anyString(), anyLong()); - - var document = document("member/phone-number-check", 204) - .request(requestDocument) - .build(); - - given(document) - .contentType(ContentType.JSON) - .header(HttpHeaders.AUTHORIZATION, accessToken()) - .queryParam("phoneNumber", "01098765432") - .when().get("/api/member/phone-number/check") - .then().statusCode(204); - } - } - - @Nested - class UpdateMember { - - RestDocsRequest requestDocument = request() - .tag(Tag.MEMBER_API) - .summary("ํšŒ์› ์ •๋ณด ์ˆ˜์ •") - .requestHeader( - headerWithName(HttpHeaders.AUTHORIZATION).description("์•ก์„ธ์Šค ํ† ํฐ") - ).requestBodyField( - fieldWithPath("nickname").type(STRING).description("ํšŒ์› ๋‹‰๋„ค์ž„"), - fieldWithPath("phoneNumber").type(STRING).description("ํšŒ์› ์ „ํ™”๋ฒˆํ˜ธ ex) 01012345678"), - fieldWithPath("interestArea").type(STRING).description("ํšŒ์› ๊ด€์‹ฌ ์ง€์—ญ ex) ์ข…๋กœ๊ตฌ"), - fieldWithPath("optInMarketing").type(BOOLEAN).description("๋งˆ์ผ€ํŒ… ๋™์˜ ์—ฌ๋ถ€") - ); - - RestDocsResponse responseDocument = response() - .responseBodyField( - fieldWithPath("id").type(NUMBER).description("ํšŒ์› ์‹๋ณ„์ž"), - fieldWithPath("isSignUp").type(BOOLEAN).description("ํšŒ์› ๊ฐ€์ž… ์š”์ฒญ ์—ฌ๋ถ€ (false ๊ณ ์ •)"), - fieldWithPath("nickname").type(STRING).description("ํšŒ์› ๋‹‰๋„ค์ž„").optional(), - fieldWithPath("phoneNumber").type(STRING).description("ํšŒ์› ์ „ํ™”๋ฒˆํ˜ธ ex) 01012345678").optional(), - fieldWithPath("interestArea").type(STRING).description("ํšŒ์› ๊ด€์‹ฌ ์ง€์—ญ ex) ์ข…๋กœ๊ตฌ").optional(), - fieldWithPath("optInMarketing").type(BOOLEAN).description("๋งˆ์ผ€ํŒ… ๋™์˜ ์—ฌ๋ถ€").optional() - ); - - @Test - void ํšŒ์›_์ •๋ณด_์ˆ˜์ •_์„ฑ๊ณต() { - MemberUpdateRequest request = new MemberUpdateRequest("update-nickname", "01012345678", "์„ฑ๋ถ๊ตฌ", true); - MemberResponse response = new MemberResponse(1L, false, "update-nickname", "01012345678", "์„ฑ๋ถ๊ตฌ", true); - doReturn(response).when(memberService).update(anyLong(), eq(request)); - - var document = document("member/update", 200) - .request(requestDocument) - .response(responseDocument) - .build(); - - given(document) - .contentType(ContentType.JSON) - .header(HttpHeaders.AUTHORIZATION, accessToken()) - .body(request) - .when().put("/api/member") - .then().statusCode(200); - } - } -} diff --git a/src/test/java/timeeat/domain/bookmark/BookmarkTest.java b/src/test/java/timeeat/domain/bookmark/BookmarkTest.java deleted file mode 100644 index 510f0452..00000000 --- a/src/test/java/timeeat/domain/bookmark/BookmarkTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package timeeat.domain.bookmark; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.time.LocalTime; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import timeeat.domain.member.Member; -import timeeat.domain.store.Store; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; - -class BookmarkTest { - - @Nested - @DisplayName("๋ถ๋งˆํฌ ์ƒ์„ฑ ์‹œ") - class CreateBookmark { - - @Test - void ์ •์ƒ์ ์ธ_๋ฉค๋ฒ„์™€_๊ฐ€๊ฒŒ๋กœ_๋ถ๋งˆํฌ๋ฅผ_์ƒ์„ฑํ•œ๋‹ค() { - Member member = new Member("socialId123", "nickname"); - Store store = new Store( - "๊ฐ€๊ฒŒ๋ช…", - "์–‘์‹", - "์ฃผ์†Œ", - 37.5, - 127.0, - "0212345678", - null, - LocalTime.of(11, 30), - LocalTime.of(21, 0), - null, - "๊ฐ•๋‚จ๊ตฌ"); - - Bookmark bookmark = new Bookmark(member, store); - - assertAll( - () -> assertThat(bookmark.getMember()).isEqualTo(member), - () -> assertThat(bookmark.getStore()).isEqualTo(store) - ); - } - - @Test - void ๋ฉค๋ฒ„๊ฐ€_null์ด๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - Store store = new Store( - "๊ฐ€๊ฒŒ๋ช…", - "์–‘์‹", - "์ฃผ์†Œ", - 37.5, - 127.0, - "0212345678", - null, - LocalTime.of(11, 30), - LocalTime.of(21, 0), - null, - "๊ฐ•๋‚จ๊ตฌ"); - - BusinessException exception = assertThrows(BusinessException.class, () -> new Bookmark(null, store)); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.BOOKMARK_MEMBER_REQUIRED); - } - - @Test - void ๊ฐ€๊ฒŒ๊ฐ€_null์ด๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - Member member = new Member("socialId123", "nickname"); - - BusinessException exception = assertThrows(BusinessException.class, () -> new Bookmark(member, null)); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.BOOKMARK_STORE_REQUIRED); - } - } -} diff --git a/src/test/java/timeeat/domain/menu/DiscountTest.java b/src/test/java/timeeat/domain/menu/DiscountTest.java deleted file mode 100644 index 18842dad..00000000 --- a/src/test/java/timeeat/domain/menu/DiscountTest.java +++ /dev/null @@ -1,86 +0,0 @@ -package timeeat.domain.menu; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.time.LocalDateTime; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; - -class DiscountTest { - - private Price originalPrice; - - @BeforeEach - void setUp() { - originalPrice = new Price(10000); - } - - @Nested - @DisplayName("ํ• ์ธ์„ ์ƒ์„ฑํ•  ๋•Œ") - class CreateDiscount { - - @Test - void ๋ชจ๋“ _์ •๋ณด๊ฐ€_์ •์ƒ์ ์ผ_๋•Œ_์ƒ์„ฑํ•œ๋‹ค() { - Integer discountPrice = 8000; - LocalDateTime startTime = LocalDateTime.now(); - LocalDateTime endTime = startTime.plusHours(2); - - Discount discount = new Discount(originalPrice, discountPrice, startTime, endTime); - - assertThat(discount.getDiscountPrice()).isEqualTo(discountPrice); - assertThat(discount.getStartTime()).isEqualTo(startTime); - assertThat(discount.getEndTime()).isEqualTo(endTime); - } - - @Test - void ํ• ์ธ_์ •๋ณด๊ฐ€_์—†์–ด๋„_์ƒ์„ฑํ•œ๋‹ค() { - Discount discount = new Discount(originalPrice, null, null, null); - - assertThat(discount.getDiscountPrice()).isNull(); - assertThat(discount.getStartTime()).isNull(); - assertThat(discount.getEndTime()).isNull(); - } - - @Test - void ํ• ์ธ_๊ฐ€๊ฒฉ์ด_์›๊ฐ€๋ณด๋‹ค_๋น„์‹ธ๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - Integer invalidDiscountPrice = 12000; - - BusinessException exception = assertThrows(BusinessException.class, () -> new Discount(originalPrice, invalidDiscountPrice, null, null)); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_MENU_DISCOUNT_PRICE); - } - - @Test - void ํ• ์ธ_๊ฐ€๊ฒฉ์ด_์›๊ฐ€์™€_๊ฐ™์œผ๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - Integer sameDiscountPrice = 10000; - - BusinessException exception = assertThrows(BusinessException.class, () -> new Discount(originalPrice, sameDiscountPrice, null, null)); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_MENU_DISCOUNT_PRICE); - } - - @Test - void ํ• ์ธ_๊ฐ€๊ฒฉ์ด_0์›์ด๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - Integer zeroDiscountPrice = 0; - - BusinessException exception = assertThrows(BusinessException.class, () -> new Discount(originalPrice, zeroDiscountPrice, null, null)); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_MENU_DISCOUNT_PRICE); - } - - @Test - void ์‹œ์ž‘_์‹œ๊ฐ„์ด_์ข…๋ฃŒ_์‹œ๊ฐ„๋ณด๋‹ค_๋Šฆ์œผ๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - LocalDateTime startTime = LocalDateTime.now(); - LocalDateTime endTime = startTime.minusHours(2); - - BusinessException exception = assertThrows(BusinessException.class, () -> new Discount(originalPrice, 8000, startTime, endTime)); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_MENU_DISCOUNT_TIME); - } - } -} diff --git a/src/test/java/timeeat/domain/menu/MenuTest.java b/src/test/java/timeeat/domain/menu/MenuTest.java deleted file mode 100644 index 9eb8d520..00000000 --- a/src/test/java/timeeat/domain/menu/MenuTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package timeeat.domain.menu; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.time.LocalDateTime; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; - -class MenuTest { - - @Nested - @DisplayName("๋ฉ”๋‰ด๋ฅผ ์ƒ์„ฑํ•  ๋•Œ") - class CreateMenu { - - @Test - void ๋ชจ๋“ _์ •๋ณด๊ฐ€_์ •์ƒ์ ์ผ_๋•Œ_๋ฉ”๋‰ด๋ฅผ_์ƒ์„ฑํ•œ๋‹ค() { - String name = "ํ•ด๋ฌผ ๋ณถ์Œ๋ฐฅ"; - Integer price = 15000; - Integer discountPrice = 12000; - - Menu menu = new Menu(name, "์„ค๋ช…", price, "url", discountPrice, LocalDateTime.now(), - LocalDateTime.now().plusHours(1)); - - assertAll( - () -> assertThat(menu.getName()).isEqualTo(name), - () -> assertThat(menu.getPrice().getValue()).isEqualTo(price), - () -> assertThat(menu.getDiscount().getDiscountPrice()).isEqualTo(discountPrice) - ); - } - - @Test - void ์ด๋ฆ„์ด_null์ด๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - BusinessException exception = assertThrows(BusinessException.class, - () -> new Menu(null, "์„ค๋ช…", 10000, "url", null, null, null)); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_MENU_NAME); - } - - @Test - void ํ• ์ธ_๊ฐ€๊ฒฉ์ด_์›๊ฐ€๋ณด๋‹ค_๋น„์‹ธ๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - Integer price = 10000; - Integer invalidDiscountPrice = 12000; - - BusinessException exception = assertThrows(BusinessException.class, - () -> new Menu("์ด๋ฆ„", "์„ค๋ช…", price, "url", invalidDiscountPrice, null, null)); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_MENU_DISCOUNT_PRICE); - } - - @Test - void ์ด๋ฆ„์ด_์ตœ๋Œ€_๊ธธ์ด๋ฅผ_์ดˆ๊ณผํ•˜๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - String overflowName = "t".repeat(255 + 1); - - BusinessException exception = assertThrows(BusinessException.class, - () -> new Menu(overflowName, "์„ค๋ช…", 10000, "url", null, null, null)); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_MENU_LENGTH); - } - } -} diff --git a/src/test/java/timeeat/domain/menu/PriceTest.java b/src/test/java/timeeat/domain/menu/PriceTest.java deleted file mode 100644 index fb22f78e..00000000 --- a/src/test/java/timeeat/domain/menu/PriceTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package timeeat.domain.menu; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; - -class PriceTest { - - @Nested - @DisplayName("๊ฐ€๊ฒฉ์„ ์ƒ์„ฑํ•  ๋•Œ") - class CreatePrice { - - @Test - void ์ •์ƒ์ ์ธ_๊ฐ€๊ฒฉ์œผ๋กœ_์ƒ์„ฑํ•œ๋‹ค() { - Integer validPriceValue = 10000; - - Price price = new Price(validPriceValue); - - assertThat(price.getValue()).isEqualTo(validPriceValue); - } - - @Test - void ๊ฐ€๊ฒฉ์ด_null์ด๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - BusinessException exception = assertThrows(BusinessException.class, () -> new Price(null)); - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_MENU_PRICE); - } - - @Test - void ๊ฐ€๊ฒฉ์ด_0์›_์ดํ•˜์ด๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - BusinessException exception = assertThrows(BusinessException.class, () -> new Price(0)); - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_MENU_PRICE); - } - } -} diff --git a/src/test/java/timeeat/domain/store/StoreHoursTest.java b/src/test/java/timeeat/domain/store/StoreHoursTest.java deleted file mode 100644 index 36d8ad6f..00000000 --- a/src/test/java/timeeat/domain/store/StoreHoursTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package timeeat.domain.store; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.time.LocalTime; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; - -class StoreHoursTest { - - @Nested - @DisplayName("์˜์—…์‹œ๊ฐ„ ์ƒ์„ฑ ์‹œ") - class CreateStoreHours { - - @Test - void ์ •์ƒ์ ์ธ_์˜์—…์‹œ๊ฐ„์œผ๋กœ_์ƒ์„ฑํ•œ๋‹ค() { - LocalTime open = LocalTime.of(9, 0); - LocalTime close = LocalTime.of(18, 0); - - StoreHours storeHours = new StoreHours(open, close); - - assertThat(storeHours.getOpenTime()).isEqualTo(open); - assertThat(storeHours.getCloseTime()).isEqualTo(close); - } - - @Test - void openTime์ด_null์ด๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - LocalTime close = LocalTime.of(18, 0); - - BusinessException exception = assertThrows(BusinessException.class, () -> new StoreHours(null, close)); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_STORE_TIME_NULL); - } - - @Test - void closeTime์ด_null์ด๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - LocalTime open = LocalTime.of(9, 0); - - BusinessException exception = assertThrows(BusinessException.class, () -> new StoreHours(open, null)); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_STORE_TIME_NULL); - } - - @Test - void openTime์ด_closeTime๋ณด๋‹ค_๋Šฆ์œผ๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - LocalTime open = LocalTime.of(20, 0); - LocalTime close = LocalTime.of(9, 0); - - BusinessException exception = assertThrows(BusinessException.class, () -> new StoreHours(open, close)); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_STORE_TIME_ORDER); - } - } -} diff --git a/src/test/java/timeeat/domain/store/StorePhoneNumberTest.java b/src/test/java/timeeat/domain/store/StorePhoneNumberTest.java deleted file mode 100644 index f1e34ff7..00000000 --- a/src/test/java/timeeat/domain/store/StorePhoneNumberTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package timeeat.domain.store; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; - -class StorePhoneNumberTest { - - @Nested - @DisplayName("๊ฐ€๊ฒŒ ๋ฒˆํ˜ธ๋ฅผ ์ƒ์„ฑํ•  ๋•Œ") - class CreateStorePhoneNumber { - - @ParameterizedTest - @ValueSource(strings = {"021234567", "03112345678", "15771234", "077012345678"}) - void ๊ฐ€๊ฒŒ_๋ฒˆํ˜ธ๋ฅผ_์ •์ƒ์ ์œผ๋กœ_์ƒ์„ฑํ•œ๋‹ค(String validPhoneNumber) { - StorePhoneNumber phoneNumber = new StorePhoneNumber(validPhoneNumber); - - assertThat(phoneNumber.getValue()).isEqualTo(validPhoneNumber); - } - - @Test - void null_๊ฐ’์œผ๋กœ_์ƒ์„ฑํ• _์ˆ˜_์žˆ๋‹ค() { - String nullNumber = null; - - StorePhoneNumber phoneNumber = new StorePhoneNumber(nullNumber); - - assertThat(phoneNumber.getValue()).isNull(); - } - - @ParameterizedTest - @ValueSource(strings = {"1234567", "1234567890123", "02-1234-5678", "abcdefghij"}) - void ๋ฒˆํ˜ธ_ํ˜•์‹์ด_์˜ฌ๋ฐ”๋ฅด์ง€_์•Š์œผ๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค(String invalidNumber) { - BusinessException exception = assertThrows(BusinessException.class, () -> new StorePhoneNumber(invalidNumber)); - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_STORE_PHONE_NUMBER); - } - } -} diff --git a/src/test/java/timeeat/domain/store/StoreTest.java b/src/test/java/timeeat/domain/store/StoreTest.java deleted file mode 100644 index 696e1902..00000000 --- a/src/test/java/timeeat/domain/store/StoreTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package timeeat.domain.store; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.time.LocalTime; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import timeeat.enums.StoreCategory; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; - -class StoreTest { - - @Nested - @DisplayName("๊ฐ€๊ฒŒ ์ •๋ณด๋ฅผ ์ƒ์„ฑํ•  ๋•Œ") - class CreateStore { - - @Test - void ๋ชจ๋“ _์ •๋ณด๊ฐ€_์ •์ƒ์ ์ผ_๋•Œ_๊ฐ€๊ฒŒ๋ฅผ_์ƒ์„ฑํ•œ๋‹ค() { - String name = "์ง„๋˜๋ฐฐ๊ธฐ ํŒŒ์Šคํƒ€"; - String category = "์–‘์‹"; - String address = "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 212"; - Double latitude = 37.5012; - Double longitude = 127.0396; - String phoneNumber = "0212345678"; - LocalTime openTime = LocalTime.of(11, 30); - LocalTime closeTime = LocalTime.of(21, 0); - - Store store = new Store(name, category, address, latitude, longitude, - phoneNumber, null, openTime, closeTime, null, "๊ฐ•๋‚จ๊ตฌ"); - - assertAll( - () -> assertThat(store.getName()).isEqualTo(name), - () -> assertThat(store.getCategory()).isEqualTo(StoreCategory.WESTERN), - () -> assertThat(store.getAddress()).isEqualTo(address), - () -> assertThat(store.getCoordinates().getLatitude()).isEqualTo(latitude), - () -> assertThat(store.getStorePhoneNumber().getValue()).isEqualTo(phoneNumber), - () -> assertThat(store.getStoreHours().getOpenTime()).isEqualTo(openTime) - ); - } - - @Test - void ๊ฐ€๊ฒŒ_์ด๋ฆ„์ด_null์ด๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - BusinessException exception = assertThrows(BusinessException.class, - () -> new Store(null, "์–‘์‹", "์ฃผ์†Œ", 37.5, 127.0, "0212345678", null, LocalTime.now(), LocalTime.now().plusHours(1), null, "๊ฐ•๋‚จ๊ตฌ")); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_STORE_NAME); - } - - @Test - void ์ฃผ์†Œ๊ฐ€_null์ด๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - BusinessException exception = assertThrows(BusinessException.class, - () -> new Store("๊ฐ€๊ฒŒ", "์–‘์‹", null, 37.5, 127.0, "0212345678", null, LocalTime.now(), LocalTime.now().plusHours(1), null, "๊ฐ•๋‚จ๊ตฌ")); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_STORE_ADDRESS); - } - - @Test - void ์œ ํšจํ•˜์ง€_์•Š์€_์นดํ…Œ๊ณ ๋ฆฌ๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - BusinessException exception = assertThrows(BusinessException.class, - () -> new Store("๊ฐ€๊ฒŒ", "์—†๋Š”์นดํ…Œ๊ณ ๋ฆฌ", "์ฃผ์†Œ", 37.5, 127.0, "0212345678", null, LocalTime.now(), LocalTime.now().plusHours(1), null, "๊ฐ•๋‚จ๊ตฌ")); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_STORE_CATEGORY); - } - - @Test - void ์ขŒํ‘œ๊ฐ’์ด_null์ด๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - BusinessException exception = assertThrows(BusinessException.class, - () -> new Store("๊ฐ€๊ฒŒ", "์–‘์‹", "์ฃผ์†Œ", null, 127.0, "0212345678", null, LocalTime.now(), LocalTime.now().plusHours(1), null, "๊ฐ•๋‚จ๊ตฌ")); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_STORE_COORDINATES_NULL); - } - - @Test - void ์ „ํ™”๋ฒˆํ˜ธ_ํ˜•์‹์ด_ํ‹€๋ฆฌ๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - BusinessException exception = assertThrows(BusinessException.class, - () -> new Store("๊ฐ€๊ฒŒ", "์–‘์‹", "์ฃผ์†Œ", 37.5, 127.0, "123", null, LocalTime.now(), LocalTime.now().plusHours(1), null, "๊ฐ•๋‚จ๊ตฌ")); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_STORE_PHONE_NUMBER); - } - - @Test - void ์˜์—…_์‹œ๊ฐ„_์ˆœ์„œ๊ฐ€_ํ‹€๋ฆฌ๋ฉด_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - LocalTime openTime = LocalTime.of(22, 0); - LocalTime closeTime = LocalTime.of(10, 0); - - BusinessException exception = assertThrows(BusinessException.class, - () -> new Store("๊ฐ€๊ฒŒ", "์–‘์‹", "์ฃผ์†Œ", 37.5, 127.0, "0212345678", null, openTime, closeTime, null, "๊ฐ•๋‚จ๊ตฌ")); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_STORE_TIME_ORDER); - } - } -} diff --git a/src/test/java/timeeat/enums/InterestAreaTest.java b/src/test/java/timeeat/enums/InterestAreaTest.java deleted file mode 100644 index c7f101b9..00000000 --- a/src/test/java/timeeat/enums/InterestAreaTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package timeeat.enums; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import timeeat.exception.BusinessErrorCode; -import timeeat.exception.BusinessException; - -class InterestAreaTest { - - @Nested - @DisplayName("from ๋ฉ”์„œ๋“œ ํ…Œ์ŠคํŠธ") - class FromMethod { - - @Test - void ์œ ํšจํ•œ_์ด๋ฆ„์œผ๋กœ_InterestArea_๊ฐ์ฒด๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { - assertThat(InterestArea.from("๊ฐ•๋‚จ๊ตฌ")).isEqualTo(InterestArea.GANGNAM); - assertThat(InterestArea.from("์„œ์ดˆ๊ตฌ")).isEqualTo(InterestArea.SEOCHO); - } - - @Test - void ์œ ํšจํ•˜์ง€_์•Š์€_์ด๋ฆ„์œผ๋กœ_์˜ˆ์™ธ๋ฅผ_๋˜์ง„๋‹ค() { - BusinessException exception = assertThrows(BusinessException.class, () -> InterestArea.from("์—†๋Š”๋™๋„ค")); - - assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.INVALID_INTEREST_AREA); - } - } -} diff --git a/src/test/java/timeeat/fixture/MemberGenerator.java b/src/test/java/timeeat/fixture/MemberGenerator.java deleted file mode 100644 index 8aa0b701..00000000 --- a/src/test/java/timeeat/fixture/MemberGenerator.java +++ /dev/null @@ -1,30 +0,0 @@ -package timeeat.fixture; - -import org.springframework.stereotype.Component; -import timeeat.domain.member.Member; -import timeeat.enums.InterestArea; -import timeeat.repository.member.MemberRepository; - -@Component -public class MemberGenerator { - - private static final String DEFAULT_INTEREST_AREA = InterestArea.SEONGBUK.getAreaName(); - - private final MemberRepository memberRepository; - - public MemberGenerator(MemberRepository memberRepository) { - this.memberRepository = memberRepository; - } - - public Member generate(String socialId) { - return memberRepository.save(new Member(socialId, "nickname")); - } - - public Member generate(String socialId, String nickname) { - return memberRepository.save(new Member(socialId, nickname)); - } - - public Member generateRegisteredMember(String socialId, String nickname, String phoneNumber) { - return memberRepository.save(new Member(socialId, nickname, phoneNumber, DEFAULT_INTEREST_AREA, true)); - } -} diff --git a/src/test/java/timeeat/repository/BaseRepositoryTest.java b/src/test/java/timeeat/repository/BaseRepositoryTest.java deleted file mode 100644 index 3a512c09..00000000 --- a/src/test/java/timeeat/repository/BaseRepositoryTest.java +++ /dev/null @@ -1,16 +0,0 @@ -package timeeat.repository; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import timeeat.fixture.MemberGenerator; -import timeeat.repository.member.MemberRepository; - -@DataJpaTest -public abstract class BaseRepositoryTest { - - @Autowired - protected MemberGenerator memberGenerator; - - @Autowired - protected MemberRepository memberRepository; -} diff --git a/src/test/java/timeeat/service/BaseServiceTest.java b/src/test/java/timeeat/service/BaseServiceTest.java deleted file mode 100644 index d376f1ca..00000000 --- a/src/test/java/timeeat/service/BaseServiceTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package timeeat.service; - -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doReturn; - -import java.net.URI; -import java.net.URISyntaxException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.bean.override.mockito.MockitoBean; -import timeeat.DatabaseCleaner; -import timeeat.client.oauth.OauthClient; -import timeeat.client.oauth.OauthMemberInformation; -import timeeat.client.oauth.OauthToken; -import timeeat.fixture.MemberGenerator; -import timeeat.repository.member.MemberRepository; - -@ExtendWith(DatabaseCleaner.class) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) -public abstract class BaseServiceTest { - - private static final OauthToken DEFAULT_OAUTH_TOKEN = new OauthToken("oauth-access-token"); - private static final OauthMemberInformation DEFAULT_OAUTH_MEMBER_INFO = - new OauthMemberInformation(123L, "nickname"); - - @Autowired - protected MemberGenerator memberGenerator; - - @Autowired - protected MemberRepository memberRepository; - - @MockitoBean - private OauthClient oauthClient; - - @BeforeEach - protected final void mockingClient() throws URISyntaxException { - doReturn(new URI("http://localhost:8080/login/callback")).when(oauthClient).getOauthLoginUrl(anyString()); - doReturn(DEFAULT_OAUTH_TOKEN).when(oauthClient).requestOauthToken(anyString(), anyString()); - doReturn(DEFAULT_OAUTH_MEMBER_INFO).when(oauthClient).requestMemberInformation(DEFAULT_OAUTH_TOKEN); - } -} diff --git a/src/test/resources/application-flyway.yml b/src/test/resources/application-flyway.yml new file mode 100644 index 00000000..d04eda7b --- /dev/null +++ b/src/test/resources/application-flyway.yml @@ -0,0 +1,13 @@ +spring: + datasource: + driver-class-name: org.h2.Driver + username: sa + password: + + jpa: + hibernate: + ddl-auto: validate + + flyway: + enabled: true + baseline-on-migrate: false diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index eb236fc9..62cf1e9b 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -11,6 +11,13 @@ spring: import: - optional:file:.env-local[.properties] + cloud: + aws: + region: + static: ap-northeast-2 + s3: + bucket: eatda-storage-test + datasource: driver-class-name: org.h2.Driver url: jdbc:h2:mem:database @@ -45,3 +52,6 @@ oauth: redirect-path: /callback allowed-origins: - "https://example.eat-da.com" + +kakao: + api-key: ablselfsgbwrlgrwrlhknwnkefknkwelfk diff --git a/terraform/dev/ec2/main.tf b/terraform/dev/ec2/main.tf index 0c250ba6..42fe8a70 100644 --- a/terraform/dev/ec2/main.tf +++ b/terraform/dev/ec2/main.tf @@ -6,7 +6,7 @@ resource "aws_instance" "dev" { key_name = var.instance_definitions.key_name user_data = var.instance_definitions.user_data vpc_security_group_ids = [var.ec2_sg_id] - user_data_replace_on_change = true + user_data_replace_on_change = false metadata_options { http_tokens = "required" diff --git a/terraform/dev/locals.tf b/terraform/dev/locals.tf index 7521c329..65750768 100644 --- a/terraform/dev/locals.tf +++ b/terraform/dev/locals.tf @@ -23,8 +23,14 @@ locals { environment = "dev" name_prefix = "eatda" + bucket_name_prefix = "eatda-storage" + allowed_origins = [ + "https://dev.eatda.net", + "http://localhost:3000" + ] + ec2_sg_id = data.terraform_remote_state.common.outputs.security_group_ids["ec2"] - instance_subnet_map = data.terraform_remote_state.common.outputs.public_subnet_ids # ์—๋Ÿฌ๊ฐ€ ๋‚ฌ๋˜ ๋ถ€๋ถ„ + instance_subnet_map = data.terraform_remote_state.common.outputs.public_subnet_ids ecr_repo_urls = data.terraform_remote_state.bootstrap.outputs.ecr_repo_urls ecs_services = var.ecs_services alb_target_group_arns = data.terraform_remote_state.common.outputs.target_group_arns diff --git a/terraform/dev/main.tf b/terraform/dev/main.tf index 50f03f5a..192a1b09 100644 --- a/terraform/dev/main.tf +++ b/terraform/dev/main.tf @@ -16,3 +16,10 @@ module "ecs" { environment = local.environment tags = local.common_tags } + +module "s3" { + source = "./s3" + bucket_name_prefix = local.bucket_name_prefix + environment = local.environment + allowed_origins = local.allowed_origins +} diff --git a/terraform/dev/s3/main.tf b/terraform/dev/s3/main.tf new file mode 100644 index 00000000..c4253bcf --- /dev/null +++ b/terraform/dev/s3/main.tf @@ -0,0 +1,40 @@ +resource "aws_s3_bucket" "dev" { + bucket = "${var.bucket_name_prefix}-${var.environment}" + + tags = { + Name = "${var.bucket_name_prefix}-${var.environment}" + Environment = var.environment + ManagedBy = "Terraform" + } +} + +resource "aws_s3_bucket_public_access_block" "dev" { + bucket = aws_s3_bucket.dev.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "dev" { + bucket = aws_s3_bucket.dev.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +resource "aws_s3_bucket_cors_configuration" "dev" { + bucket = aws_s3_bucket.dev.id + + cors_rule { + allowed_headers = ["*"] + allowed_methods = ["GET"] + allowed_origins = var.allowed_origins + expose_headers = ["ETag"] + max_age_seconds = 3000 + } +} diff --git a/terraform/dev/s3/outputs.tf b/terraform/dev/s3/outputs.tf new file mode 100644 index 00000000..e69de29b diff --git a/terraform/dev/s3/variables.tf b/terraform/dev/s3/variables.tf new file mode 100644 index 00000000..e2279053 --- /dev/null +++ b/terraform/dev/s3/variables.tf @@ -0,0 +1,11 @@ +variable "bucket_name_prefix" { + type = string +} + +variable "environment" { + type = string +} + +variable "allowed_origins" { + type = list(string) +} diff --git a/terraform/prod/ec2/main.tf b/terraform/prod/ec2/main.tf index a48228fd..d5183697 100644 --- a/terraform/prod/ec2/main.tf +++ b/terraform/prod/ec2/main.tf @@ -6,7 +6,7 @@ resource "aws_instance" "prod" { key_name = var.instance_definitions.key_name user_data = var.instance_definitions.user_data vpc_security_group_ids = [var.ec2_sg_id] - user_data_replace_on_change = true + user_data_replace_on_change = false metadata_options { http_tokens = "required" @@ -25,4 +25,4 @@ resource "aws_eip" "prod" { tags = { Name = "${var.name_prefix}-${var.instance_definitions.role}-eip" } -} \ No newline at end of file +} diff --git a/terraform/prod/locals.tf b/terraform/prod/locals.tf index a10816fa..3af96ec9 100644 --- a/terraform/prod/locals.tf +++ b/terraform/prod/locals.tf @@ -23,6 +23,12 @@ locals { environment = "prod" name_prefix = "eatda" + bucket_name_prefix = "eatda-storage" + allowed_origins = [ + "https://eatda.net", + "https://www.eatda.net" + ] + ec2_sg_id = data.terraform_remote_state.common.outputs.security_group_ids["ec2"] instance_definitions = data.terraform_remote_state.common.outputs.instance_profile_name["ec2-to-ecs"] instance_subnet_map = data.terraform_remote_state.common.outputs.public_subnet_ids diff --git a/terraform/prod/main.tf b/terraform/prod/main.tf index b6aea617..ce4a01d3 100644 --- a/terraform/prod/main.tf +++ b/terraform/prod/main.tf @@ -46,3 +46,10 @@ module "rds" { storage_encrypted = true tags = local.common_tags } + +module "s3" { + source = "./s3" + bucket_name_prefix = local.bucket_name_prefix + environment = local.environment + allowed_origins = local.allowed_origins +} diff --git a/terraform/prod/s3/main.tf b/terraform/prod/s3/main.tf new file mode 100644 index 00000000..5fced905 --- /dev/null +++ b/terraform/prod/s3/main.tf @@ -0,0 +1,40 @@ +resource "aws_s3_bucket" "prod" { + bucket = "${var.bucket_name_prefix}-${var.environment}" + + tags = { + Name = "${var.bucket_name_prefix}-${var.environment}" + Environment = var.environment + ManagedBy = "Terraform" + } +} + +resource "aws_s3_bucket_public_access_block" "prod" { + bucket = aws_s3_bucket.prod.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "prod" { + bucket = aws_s3_bucket.prod.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +resource "aws_s3_bucket_cors_configuration" "prod" { + bucket = aws_s3_bucket.prod.id + + cors_rule { + allowed_headers = ["*"] + allowed_methods = ["GET"] + allowed_origins = var.allowed_origins + expose_headers = ["ETag"] + max_age_seconds = 3000 + } +} diff --git a/terraform/prod/s3/outputs.tf b/terraform/prod/s3/outputs.tf new file mode 100644 index 00000000..e69de29b diff --git a/terraform/prod/s3/variables.tf b/terraform/prod/s3/variables.tf new file mode 100644 index 00000000..e2279053 --- /dev/null +++ b/terraform/prod/s3/variables.tf @@ -0,0 +1,11 @@ +variable "bucket_name_prefix" { + type = string +} + +variable "environment" { + type = string +} + +variable "allowed_origins" { + type = list(string) +}