feat: 스팟/체크인/한줄평 API 및 로컬(H2) 실행 프로파일

- GET /api/spots, POST /{id}/checkins, GET /{id}/mates, GET·POST /{id}/reviews
- Spot/CheckIn/SpotReview 엔티티·서비스 (체크인 2시간 TTL, 만료 비노출)
- CORS 허용(Vite dev, Capacitor iOS/AOS)
- local 프로파일: Postgres 없이 H2로 기동 + 시드 데이터 (연동 검증용)
- V2 마이그레이션: spots 위경도 컬럼 추가, geom NOT NULL 완화

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
sb
2026-07-11 08:07:43 +09:00
parent 1befad06d2
commit 115bc13ea1
15 changed files with 550 additions and 1 deletions
@@ -0,0 +1,67 @@
package com.dog.dognation.api;
import com.dog.dognation.api.dto.SpotDtos.CheckInRequest;
import com.dog.dognation.api.dto.SpotDtos.CheckInResponse;
import com.dog.dognation.api.dto.SpotDtos.MateResponse;
import com.dog.dognation.api.dto.SpotDtos.ReviewCreateRequest;
import com.dog.dognation.api.dto.SpotDtos.ReviewResponse;
import com.dog.dognation.api.dto.SpotDtos.SpotResponse;
import com.dog.dognation.domain.spot.SpotService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/spots")
public class SpotController {
private final SpotService spotService;
public SpotController(SpotService spotService) {
this.spotService = spotService;
}
/** 스팟 목록 */
@GetMapping
public List<SpotResponse> list() {
return spotService.listSpots().stream().map(SpotResponse::from).toList();
}
/** 스팟 체크인 ("나 지금 여기 도착!") */
@PostMapping("/{id}/checkins")
public ResponseEntity<CheckInResponse> checkIn(@PathVariable Long id,
@Valid @RequestBody CheckInRequest req) {
CheckInResponse body = CheckInResponse.from(
spotService.checkIn(id, req.userId(), req.dogId()));
return ResponseEntity.status(HttpStatus.CREATED).body(body);
}
/** 스팟에 현재 체크인 중인 메이트 목록 */
@GetMapping("/{id}/mates")
public List<MateResponse> mates(@PathVariable Long id) {
return spotService.activeMates(id).stream().map(MateResponse::from).toList();
}
/** 스팟 한줄평 목록 */
@GetMapping("/{id}/reviews")
public List<ReviewResponse> reviews(@PathVariable Long id) {
return spotService.reviews(id).stream().map(ReviewResponse::from).toList();
}
/** 스팟 한줄평 작성 */
@PostMapping("/{id}/reviews")
public ResponseEntity<ReviewResponse> addReview(@PathVariable Long id,
@Valid @RequestBody ReviewCreateRequest req) {
ReviewResponse body = ReviewResponse.from(
spotService.addReview(id, req.userId(), req.tag(), req.content()));
return ResponseEntity.status(HttpStatus.CREATED).body(body);
}
}