Files
tasteby/backend-java/src/main/resources/application.yml
joungmin 319fd18258 feat(stats): #337 봇 UA 필터 + IP 레이트리밋
- BotDetector 유틸 (Pattern.CASE_INSENSITIVE: bot|crawler|spider|slurp|scrap|fetch|monitor|preview|lighthouse)
- RateLimitService: Redis SET NX EX(60s) 패턴으로 같은 IP 윈도우 차단
  - Bucket4j 대신 spring-data-redis 기존 의존성 재사용 (간결)
  - Redis 다운 시 fail-open (사용자 경험 우선)
- StatsController.recordVisit: HttpServletRequest 받아 UA + X-Forwarded-For 우선 IP
  - 봇/리밋 초과 → 200 + counted:false (사용자 페이지 로드 지장 X)
  - 통과 → 200 + counted:true → statsService.recordVisit()
- application.yml: app.rate-limit.visit-window-seconds (env VISIT_WINDOW_SECONDS) 기본 60
- dev 검증: 봇 UA → counted=false, Mozilla → true, 즉시 재호출 → false

설계서: docs/design/337-stats-bot-ratelimit/README.md

Refs: #337 (Developer 단계)
2026-06-15 15:26:27 +09:00

91 lines
2.6 KiB
YAML

server:
port: 8000
spring:
threads:
virtual:
enabled: true
datasource:
url: jdbc:oracle:thin:@${ORACLE_DSN}
username: ${ORACLE_USER}
password: ${ORACLE_PASSWORD}
driver-class-name: oracle.jdbc.OracleDriver
hikari:
minimum-idle: 2
maximum-pool-size: 10
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
pool-name: TastebyHikariPool
connection-init-sql: "SELECT 1 FROM DUAL"
data:
redis:
host: ${REDIS_HOST:192.168.0.147}
port: ${REDIS_PORT:6379}
database: ${REDIS_DB:0}
timeout: 2000ms
jackson:
default-property-inclusion: non_null
property-naming-strategy: SNAKE_CASE
serialization:
write-dates-as-timestamps: false
app:
jwt:
secret: ${JWT_SECRET:tasteby-dev-secret-change-me}
expiration-days: 7
cors:
allowed-origins: http://localhost:3000,http://localhost:3001,https://www.tasteby.net,https://tasteby.net,https://dev.tasteby.net
oracle:
wallet-path: ${ORACLE_WALLET:}
oci:
compartment-id: ${OCI_COMPARTMENT_ID}
chat-endpoint: ${OCI_CHAT_ENDPOINT:https://inference.generativeai.us-ashburn-1.oci.oraclecloud.com}
embed-endpoint: ${OCI_GENAI_ENDPOINT:https://inference.generativeai.us-chicago-1.oci.oraclecloud.com}
chat-model-id: ${OCI_CHAT_MODEL_ID}
embed-model-id: ${OCI_EMBED_MODEL_ID:cohere.embed-v4.0}
google:
maps-api-key: ${GOOGLE_MAPS_API_KEY}
youtube-api-key: ${YOUTUBE_DATA_API_KEY}
client-id: ${GOOGLE_CLIENT_ID:635551099330-2l003d3ernjmkqavd4f6s78r8r405iml.apps.googleusercontent.com}
cache:
ttl-seconds: 600
search:
# #293 — 벡터 검색 cosine distance 임계값 (0.0=완전일치, 1.0=직교).
# 0.57은 cohere embed-v4 한국어 시맨틱 적합도 기준 경험값.
max-distance: ${SEARCH_MAX_DISTANCE:0.57}
rate-limit:
# #337 — 같은 IP에서 visit 카운트 허용 간격(초). 기본 60.
visit-window-seconds: ${VISIT_WINDOW_SECONDS:60}
build:
# #338 — 배포 시 deploy.sh가 env로 주입. dev에서는 dev/unknown.
version: ${APP_VERSION:dev}
commit: ${APP_COMMIT:unknown}
daemon:
# 인스턴스 차원 스케줄러 활성화. dev/prod가 같은 DB를 공유하므로
# dev .env에 DAEMON_ENABLED=false를 설정해 dev 폴링을 끄고 prod만 동작시킨다.
enabled: ${DAEMON_ENABLED:true}
mybatis:
mapper-locations: classpath:mybatis/mapper/*.xml
config-location: classpath:mybatis/mybatis-config.xml
type-aliases-package: com.tasteby.domain
type-handlers-package: com.tasteby.config
logging:
level:
com.tasteby: DEBUG
com.tasteby.mapper: DEBUG