Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
6f9c78b
๋ณด์•ˆ๊ด€๋ จ ์ˆ˜์ •
rktclgh Mar 16, 2026
352d26f
๋ณด์•ˆ๊ด€๋ จ ์ˆ˜์ •
rktclgh Mar 16, 2026
1a6ce44
์‚ฌ์šฉ์ž ์„œ๋น„์Šค ๋ชจ๋“œ ์ถ”๊ฐ€
rktclgh Mar 16, 2026
7f2ded5
๋Œ€ํ•™์ƒ ๋ชจ๋“œ ๋ชจ์˜๊ณ ์‚ฌ ์„ธ์…˜ ์ƒ์„ธ / ๋‹ต์•ˆ ์ œ์ถœ / ๊ฐ„๋‹จ ์ฑ„์  ์ €์žฅ
rktclgh Mar 16, 2026
dbee9c4
[feat] ๋Œ€ํ•™์ƒ ๋ชจ๋“œ ๋ชจ์˜๊ณ ์‚ฌ ์˜ต์…˜ ๊ณ ๋„ํ™” ๋ฐ ์ž๋ฃŒ ์‹œ๊ฐ ์ž์‚ฐ ์ถ”์ถœ ์ถ”๊ฐ€
rktclgh Mar 17, 2026
0ef63c5
์กฑ๋ณด ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์™„๋ฃŒ
rktclgh Mar 17, 2026
4a5b603
์š”์•ฝ๋ณธ ์ƒ์„ฑ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
rktclgh Mar 17, 2026
1906b61
์ฝ”๋“œ๋ž˜๋น— ๋ฆฌ๋ทฐ ๋ฐ˜์˜
rktclgh Mar 17, 2026
9711555
์ฝ”๋“œ๋ž˜๋น— ๋ฆฌ๋ทฐ ๋ฐ˜์˜
rktclgh Mar 18, 2026
fbdc67e
์ฝ”๋“œ๋ž˜๋น— ์ˆ˜์ •
rktclgh Mar 18, 2026
8e80612
์ฝ”๋“œ๋ž˜๋น— ์ˆ˜์ •, ๋Œ€ํ•™ ์กฐํšŒ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ -> api ๊ตฌ์กฐ๋กœ ์ˆ˜์ •
rktclgh Mar 18, 2026
2a02099
์ฝ”๋“œ๋ž˜๋น— ์ˆ˜์ •
rktclgh Mar 18, 2026
18b44f4
Merge pull request #69 from rktclgh/feat/v0.5
rktclgh Mar 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/deploy-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ jobs:
cache: gradle

- name: Build Spring Boot jar
env:
GRADLE_OPTS: -Dorg.gradle.vfs.watch=false
run: |
chmod +x gradlew
./gradlew clean bootJar --no-daemon
Expand Down
3 changes: 3 additions & 0 deletions deploy/fail2ban/filter.d/vlainter-probe.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[Definition]
failregex = ^<HOST> - - \[[^]]+\] "(?:GET|POST|HEAD|PUT|DELETE|OPTIONS|PATCH) [^"]+" 444
ignoreregex =
9 changes: 9 additions & 0 deletions deploy/fail2ban/jail.d/vlainter-probe.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[vlainter-probe]
enabled = true
filter = vlainter-probe
logpath = /var/log/nginx/probe-access.log
backend = auto
port = http,https
findtime = 600
maxretry = 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

ํ˜„์žฌ maxretry = 1๋กœ ์„ค์ •๋˜์–ด ์žˆ์–ด ๋‹จ ํ•œ ๋ฒˆ์˜ 444 ์‘๋‹ต์œผ๋กœ๋„ IP๊ฐ€ ์ฐจ๋‹จ๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” ํ”„๋กœ๋ธŒ ๊ณต๊ฒฉ์„ ๋น ๋ฅด๊ฒŒ ์ฐจ๋‹จํ•˜๋Š” ๋ฐ ํšจ๊ณผ์ ์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ์˜๋„์น˜ ์•Š์€ ์ •์ƒ์ ์ธ ์š”์ฒญ์ด 444๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒฝ์šฐ์—๋„ ์ฆ‰์‹œ ์ฐจ๋‹จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜คํƒ(false positive) ๊ฐ€๋Šฅ์„ฑ์„ ์ค„์ด๊ธฐ ์œ„ํ•ด maxretry ๊ฐ’์„ 2 ๋˜๋Š” 3์œผ๋กœ ๋Š˜๋ฆฌ๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•ด๋„ ์—ฌ์ „ํžˆ ๊ณต๊ฒฉ์ ์ธ ํ”„๋กœ๋ธŒ๋Š” ํšจ๊ณผ์ ์œผ๋กœ ์ฐจ๋‹จ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

bantime = 3600
80 changes: 80 additions & 0 deletions deploy/host-nginx/vlainter.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
server {
listen 80;
listen [::]:80;
server_name vlainter.online www.vlainter.online;

access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;

location /.well-known/acme-challenge/ {
root /var/www/html;
}

location ~* ^/(?:\.env(?:$|[./])|\.git(?:$|/)|phpmyadmin(?:$|/)|xmlrpc\.php$|wp-(?:admin|content|includes|login\.php|json)|.*phpunit.*|.*pearcmd.*|.*eval-stdin\.php.*) {
access_log /var/log/nginx/probe-access.log combined;
return 444;
}

location / {
return 301 https://$host$request_uri;
}
}

server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name vlainter.online www.vlainter.online;

access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;

ssl_certificate /etc/letsencrypt/live/vlainter.online/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vlainter.online/privkey.pem;

client_max_body_size 50m;

real_ip_header CF-Connecting-IP;
real_ip_recursive on;

set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;

location ~* ^/(?:\.env(?:$|[./])|\.git(?:$|/)|phpmyadmin(?:$|/)|xmlrpc\.php$|wp-(?:admin|content|includes|login\.php|json)|.*phpunit.*|.*pearcmd.*|.*eval-stdin\.php.*) {
access_log /var/log/nginx/probe-access.log combined;
return 444;
}

location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Internal-Client-IP $remote_addr;
proxy_set_header CF-Connecting-IP $http_cf_connecting_ip;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 60s;
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
}
}
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kotlin.incremental=false
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.cw.vlainter.domain.academic.controller

import com.cw.vlainter.domain.academic.dto.DepartmentSearchItemResponse
import com.cw.vlainter.domain.academic.dto.UniversitySearchItemResponse
import com.cw.vlainter.domain.academic.service.AcademicSearchService
import jakarta.validation.constraints.Size
import org.springframework.http.HttpStatus
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException

@Validated
@RestController
@RequestMapping("/api/academics")
class AcademicSearchController(
private val academicSearchService: AcademicSearchService
) {
@GetMapping("/universities/search")
fun searchUniversities(
@RequestParam
@Size(max = 120, message = "๋Œ€ํ•™๊ต ๊ฒ€์ƒ‰์–ด๋Š” 120์ž ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.")
keyword: String
): List<UniversitySearchItemResponse> {
return academicSearchService.searchUniversities(keyword)
}

@GetMapping("/departments/search")
fun searchDepartments(
@RequestParam(required = false)
universityId: String?,
@RequestParam
@Size(max = 120, message = "๋Œ€ํ•™๊ต ์ด๋ฆ„์€ 120์ž ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.")
universityName: String,
@RequestParam
@Size(max = 120, message = "ํ•™๊ณผ ๊ฒ€์ƒ‰์–ด๋Š” 120์ž ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.")
keyword: String
): List<DepartmentSearchItemResponse> {
val parsedUniversityId = parseUniversityId(universityId)
return academicSearchService.searchDepartments(
universityId = parsedUniversityId,
universityName = universityName,
keyword = keyword
)
}

private fun parseUniversityId(rawUniversityId: String?): Long? {
val normalized = rawUniversityId?.trim()
if (normalized.isNullOrEmpty()) return null
if (normalized.equals("null", ignoreCase = true)) return null
if (normalized.equals("undefined", ignoreCase = true)) return null
return normalized.toLongOrNull()
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "universityId๋Š” ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.cw.vlainter.domain.academic.dto

data class UniversitySearchItemResponse(
val universityId: Long? = null,
val universityName: String,
val universityCode: String? = null
)

data class DepartmentSearchItemResponse(
val departmentId: Long? = null,
val universityId: Long? = null,
val universityName: String,
val departmentName: String,
val departmentCode: String? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
@file:Suppress("JpaDataSourceORMInspection")

package com.cw.vlainter.domain.academic.entity

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.PrePersist
import jakarta.persistence.PreUpdate
import jakarta.persistence.Table
import java.time.OffsetDateTime

@Entity
@Table(name = "academic_departments")
class AcademicDepartment(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "university_id", nullable = false)
var university: AcademicUniversity,

@Column(name = "external_code", length = 60)
var externalCode: String? = null,

@Column(name = "name", nullable = false, length = 120)
var name: String,

@Column(name = "normalized_name", nullable = false, length = 160)
var normalizedName: String,

@Column(name = "last_synced_at")
var lastSyncedAt: OffsetDateTime? = null,

@Column(name = "created_at", nullable = false)
var createdAt: OffsetDateTime = OffsetDateTime.now(),

@Column(name = "updated_at", nullable = false)
var updatedAt: OffsetDateTime = OffsetDateTime.now()
) {
@PrePersist
fun prePersist() {
val now = OffsetDateTime.now()
createdAt = now
updatedAt = now
}

@PreUpdate
fun preUpdate() {
updatedAt = OffsetDateTime.now()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
@file:Suppress("JpaDataSourceORMInspection")

package com.cw.vlainter.domain.academic.entity

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.PrePersist
import jakarta.persistence.PreUpdate
import jakarta.persistence.Table
import jakarta.persistence.UniqueConstraint
import java.time.OffsetDateTime

@Entity
@Table(
name = "academic_universities",
uniqueConstraints = [
UniqueConstraint(name = "uk_academic_universities_normalized_name", columnNames = ["normalized_name"])
]
)
class AcademicUniversity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,

@Column(name = "external_code", length = 60, unique = true)
var externalCode: String? = null,

@Column(name = "name", nullable = false, length = 120)
var name: String,

@Column(name = "normalized_name", nullable = false, length = 160)
var normalizedName: String,

@Column(name = "last_synced_at")
var lastSyncedAt: OffsetDateTime? = null,

@Column(name = "created_at", nullable = false)
var createdAt: OffsetDateTime = OffsetDateTime.now(),

@Column(name = "updated_at", nullable = false)
var updatedAt: OffsetDateTime = OffsetDateTime.now()
) {
@PrePersist
fun prePersist() {
val now = OffsetDateTime.now()
createdAt = now
updatedAt = now
}

@PreUpdate
fun preUpdate() {
updatedAt = OffsetDateTime.now()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.cw.vlainter.domain.academic.repository

import com.cw.vlainter.domain.academic.entity.AcademicDepartment
import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param

interface AcademicDepartmentRepository : JpaRepository<AcademicDepartment, Long> {
fun findByUniversityIdAndExternalCode(universityId: Long, externalCode: String): AcademicDepartment?
fun findByUniversityIdAndNormalizedName(universityId: Long, normalizedName: String): AcademicDepartment?
fun findAllByUniversityId(universityId: Long): List<AcademicDepartment>

@Query(
"""
select d
from AcademicDepartment d
join fetch d.university u
where d.university.id = :universityId
and d.normalizedName like concat('%', :keyword, '%')
order by d.name asc
"""
)
fun searchByUniversityAndKeyword(
@Param("universityId") universityId: Long,
@Param("keyword") keyword: String,
pageable: Pageable
): List<AcademicDepartment>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.cw.vlainter.domain.academic.repository

import com.cw.vlainter.domain.academic.entity.AcademicUniversity
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param

interface AcademicUniversityRepository : JpaRepository<AcademicUniversity, Long> {
fun findByExternalCode(externalCode: String): AcademicUniversity?
fun findByNormalizedName(normalizedName: String): AcademicUniversity?

@Query(
"""
select u
from AcademicUniversity u
where u.normalizedName like concat('%', :keyword, '%')
order by u.name asc
"""
)
fun searchByKeyword(@Param("keyword") keyword: String): List<AcademicUniversity>
}
Loading
Loading