Skip to content

Commit

Permalink
Merge branch 'jason/20250113-anonymous-user' into jason/20250119-revi…
Browse files Browse the repository at this point in the history
…sed-security
  • Loading branch information
jyoo0515 committed Feb 3, 2025
2 parents f2ce6d2 + bfa7f96 commit df95471
Show file tree
Hide file tree
Showing 21 changed files with 272 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/dev-ci-cd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
- name: Gradle Check
run: ./gradlew clean check

- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
name: Upload Check Report If Failed
if: failure()
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
- name: Gradle Check
run: ./gradlew clean check

- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
name: Upload Check Report If Failed
if: failure()
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/prod-ci-cd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
- name: Gradle Check
run: ./gradlew clean check

- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
name: Upload Check Report If Failed
if: failure()
with:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ build/
.vscode/

*.tsv

venv/
8 changes: 5 additions & 3 deletions app-server/apple_login/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## 애플 클라이언트 JWT 갱신
## 애플 로그인용 Client Secret 갱신

6 개월에 한번씩 갱신해줘야 합니다
`secret.txt` 파일을 열면 team id, service id, key id, 그리고 authKey 가 저장되어 있습니다.
authKey 에 해당하는 내용을 `authkey.p8` 파일을 만들어 붙여넣고 `generate_client_secret.py` 에 적절한 값을 넣어 실행합니다.
service id, key id 그리고 authKey 는 테섭용과 실섭용이 구분되어 있으니 각각 스크립트에 입력하고 두번 실행해야 합니다.

`secret.txt` 파일에 있는 저장되어 있는 값을 추출해서 `authkey.p8` 파일을 만든 뒤 `generate_client_secret.py` 파일을 실행합니다
JWT 의 최대 유효기간이 6개월이라서 6개월마다 갱신해야 합니다.
2 changes: 0 additions & 2 deletions app-server/apple_login/generate_client_secret.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# https://jkim68888.tistory.com/7
# 클라이언트 팀으로부터 team_id, service_id, key_id, authkey.p8을 전달받아 사용하면 된다.
from time import time
import jwt

Expand Down
6 changes: 3 additions & 3 deletions app-server/apple_login/secret.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"data": "ENC[AES256_GCM,data:3R7gpfvOz1Mm5XWFf+Z5GaocwC7PGALPv/OobUholQiS13kPPiwvn4olsuc73Zw/iLHdZwDW3YhlWgJXh/xQtM6b0remdg2kT7C/7N6QS0CrIYUhRwIKgm9w6qX7xZieTs0jNnWcujIPVFRo1+gYAL1ZIovBY0ooybu2CAFYCpf1Wos9zT5e8JJOWpcg5P+vo1a2mPD8/DwIqbVHIOBR/myijIiPvKK3RIO1d+STNtqMqXLkfdwMV+VJCBC+odighmaCW4A5BQ+ZRarKo0FRFH/FL7gLWY2JHIOPCT7LPXdz8wNwvghfxpME2e1gFe1+fx4WeQysXLUmDtXtdgBLkn3r,iv:FuBztWuWEAAanPPQQ5s7VdjMjECiMZgUBRfLmInF6m4=,tag:Zp5vDbJ8Oj6HMTfQE4xF+Q==,type:str]",
"data": "ENC[AES256_GCM,data:J0t+kNXjeMsQXEI7uwV/ZIk7I1hkspRZLxWjDjrzUC5LcFO9oGK3o9oPse9WSoLs+g+gWjdGVg7R8Tg23JPgp1D/S5gNqsBQUvwdcN8JQk5Dm01LhEHqBoK5b1KONAPFKBaupnVqBJAJbq99WP7YFGgYAzVxZMpq8JN9XXAFVd0s5XsjKIHewTaSvWtR7mzM+rmI2rtuC48zZ5M7/e16SPAvFekGggqPUgBpJA65EVAS22ofHWToio87I2iLM6gD52njFW7OB7ZPb8tNMqMyPXnlMuYrlZA9n81a7h5Y7MwQeftAt9q/Ds7tZJKwsZa1078Gs3UQJQDc1LlzE0S4Fo4SIo/SazVrVIx64MGof7JrEOuDjRyOSA5mVA2dyuLl4kYBUfsgCL7Wr0Hy+5O2AV6oywmwO5NSd9PdWGn3/+Qmu7J1aNpzyUKs6i1acD825EHIvav6ESp5ym0iHFsGohxZuEbfQqQQEncdrIrBWpkuogqlJt80BGAQWhlkzwXd+Z7Hb7iWoUQxjb+hZQ0NvrWrp0087gTWDvsWxkEQFyBSv6sU2WUlKqh/Tu/WiOQCRIaO1Cpy0vXdLcNK4N3GFPz6PMHzo2FnNYhhom2gYTlDW9xiwx8sO6cfTvP6kAJM1FeQ/7x1NQQGetYzcgErfkC7TOwS6j2+tqPmaf1i5wzCxtpeWqUs/J+NDYrGxflbiKdKR15kv3pk/zgaJEpDwqR41vMgTbMYnxQZIvmw4LTDYXvfFBQ2VRoZl5qjXWv1lxx4Xb4pvCaucx0stuMnQ6FrxVsQmHQAjNNUp9gF/+e3RnD6v+gN+3tiXy7LxovcOqvWPu91K5Vtz2hri0pp0WL9xxeniwDnhJg4edVn2RUDemHzJXAmGWHG84/xPddzdSua4A==,iv:9yyM6DylFCQg2WnWToCQtnQ2s8fyYzvcRPDGxWbkZ6A=,tag:G86mLwvaPI2nRQJTvxx65Q==,type:str]",
"sops": {
"kms": [
{
Expand All @@ -13,8 +13,8 @@
"azure_kv": null,
"hc_vault": null,
"age": null,
"lastmodified": "2025-01-10T14:41:54Z",
"mac": "ENC[AES256_GCM,data:vNaoaYfc/5jR6ckOTH6BPRQP9epVX2/wV11Ti6+nyiPyGcwWd5ynd5bBul8KpKaIkfXLL7aPZp8ZsSFvG0oWhqk30itt6ev71IsOy15flYQLcBZDZOnda1Sr4FSUuRR8qiqNnp52+t5CmpBvB3kLoZvIUYaBUYR/gaoNNzgH7NU=,iv:w2GbjYWjv07jWLf6wZzse3oWyUhW/f3I0cjPh2xom1w=,tag:o/MgEdNbZHkGPBfyyNL/bg==,type:str]",
"lastmodified": "2025-01-16T16:01:58Z",
"mac": "ENC[AES256_GCM,data:sayoyeOFxCpCqjNC2S9CabRY8NVTHuakTtx0k3uD9xXQ2EKJ0hwjDLeTu3GgS9jLaCk0i3MI3Pg33v+9HSvj3YGH6cNduzsfdA9SVWrsdBFAiiZKOR4chCfnn3cHbfIUbi3ZiZRxmXRES3C3cudayxtuC8ere9T1ELeyvk7beYo=,iv:cqeuo9bkSJamlug+zIJyVOiFuzuWDoHxTmNMKQfiM+o=,tag:0qO+kltAwiB89z6H7WN2Lw==,type:str]",
"pgp": null,
"unencrypted_suffix": "_unencrypted",
"version": "3.9.3"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,7 @@ class UserApplicationService(
normalizedEmail
}
user.instagramId = instagramId?.trim()?.takeIf { it.isNotEmpty() }
user.mobilityTools.clear()
user.mobilityTools.addAll(mobilityTools)
user.mobilityTools = mobilityTools
userProfileRepository.save(user)

if (isNewsLetterSubscriptionAgreed) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class UserProfile(
var email: String?,
@Column(columnDefinition = "TEXT")
@Convert(converter = UserMobilityToolListToTextAttributeConverter::class)
val mobilityTools: MutableList<UserMobilityTool>,
var mobilityTools: List<UserMobilityTool>,
var pushToken: String? = null,
) : TimeAuditingBaseEntity() {
var deletedAt: Instant? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ package club.staircrusher.user.infra.adapter.`in`.controller
import club.staircrusher.api.spec.dto.ApiErrorResponse
import club.staircrusher.api.spec.dto.UpdateUserInfoPost200Response
import club.staircrusher.api.spec.dto.UpdateUserInfoPostRequest
import club.staircrusher.api.spec.dto.UserMobilityToolDto
import club.staircrusher.application.server_event.port.`in`.SccServerEventRecorder
import club.staircrusher.domain.server_event.NewsletterSubscribedOnSignupPayload
import club.staircrusher.stdlib.testing.SccRandom
import club.staircrusher.user.application.port.out.persistence.UserRepository
import club.staircrusher.user.application.port.out.web.subscription.StibeeSubscriptionService
import club.staircrusher.user.domain.model.UserMobilityTool
import club.staircrusher.user.infra.adapter.`in`.controller.base.UserITBase
import club.staircrusher.user.infra.adapter.`in`.converter.toDTO
import club.staircrusher.user.infra.adapter.`in`.converter.toModel
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.Test
Expand All @@ -20,9 +23,14 @@ import org.mockito.kotlin.eq
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyBlocking
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.data.repository.findByIdOrNull

class UpdateUserInfoTest : UserITBase() {
@Autowired
lateinit var userRepository: UserRepository

@MockBean
lateinit var stibeeSubscriptionService: StibeeSubscriptionService

Expand Down Expand Up @@ -171,6 +179,54 @@ class UpdateUserInfoTest : UserITBase() {
}
}

@Test
fun `mobility tools 를 여러번 업데이트 해도 잘 저장된다`() {
val user = transactionManager.doInTransaction {
testDataGenerator.createUser()
}

val changedEmail = "${SccRandom.string(32)}@staircrusher.club"
val params = UpdateUserInfoPostRequest(
nickname = user.nickname,
instagramId = user.instagramId,
email = changedEmail,
mobilityTools = listOf(UserMobilityToolDto.ELECTRIC_WHEELCHAIR, UserMobilityToolDto.PROSTHETIC_FOOT),
)
mvc
.sccRequest("/updateUserInfo", params, user = user)
.andExpect {
status {
isOk()
}
}
.apply {
transactionManager.doInTransaction {
val user = userRepository.findById(user.id).get()
Assertions.assertEquals(2, user.mobilityTools.size)
}
}

val params2 = UpdateUserInfoPostRequest(
nickname = user.nickname,
instagramId = user.instagramId,
email = changedEmail,
mobilityTools = listOf(UserMobilityToolDto.MANUAL_WHEELCHAIR),
)
mvc
.sccRequest("/updateUserInfo", params2, user = user)
.andExpect {
status {
isOk()
}
}
.apply {
transactionManager.doInTransaction {
val user = userRepository.findById(user.id).get()
Assertions.assertEquals(1, user.mobilityTools.size)
}
}
}

@Test
fun `뉴스레터 수신에 동의하면 stibee 에 연동한다`() {
val user = transactionManager.doInTransaction {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class UserController(
val isTargetUser = user.id in betaUsers
val isNotProd = SccEnv.getEnv() != SccEnv.PROD
val featureFlags: List<String> =
if (isTargetUser || isNotProd) listOf("MAP_VISIBLE", "TOILET_VISIBLE") else emptyList()
if (isTargetUser || isNotProd) listOf("MAP_VISIBLE", "TOILET_VISIBLE") else listOf("TOILET_VISIBLE")
return GetUserInfoResponseDto(
user = user.toDTO(),
flags = featureFlags,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package club.staircrusher

import club.staircrusher.place.application.port.out.web.MapsService
import club.staircrusher.place.domain.model.Place
import club.staircrusher.place.infra.adapter.out.web.KakaoMapsService
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import java.io.File
import java.time.Instant
import java.time.LocalDateTime
Expand Down Expand Up @@ -39,3 +45,28 @@ internal fun String.toInstant(): Instant {
internal fun Instant.toKstString(): String {
return this.atZone(seoulZoneId).toLocalDateTime().format(dateTimeFormatter)
}

data class FindLngLatResult(
val placeName: String,
val place: Place,
val lng: Double,
val lat: Double,
)
internal fun KakaoMapsService.findLngLat(placeNames: List<String>): List<FindLngLatResult> {
return runBlocking {
val deferredList = placeNames.map { placeName ->
async {
val place = findFirstByKeyword(placeName, MapsService.SearchByKeywordOption())
?: throw IllegalArgumentException("${placeName}에 해당하는 장소가 없습니다.")
println("station found: $placeName / ${place.name}")
FindLngLatResult(
placeName,
place,
place.location.lng,
place.location.lat,
)
}
}
awaitAll(*deferredList.toTypedArray())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package club.staircrusher.place

import club.staircrusher.findLngLat
import club.staircrusher.place.infra.adapter.out.web.KakaoMapsService
import club.staircrusher.place.infra.adapter.out.web.KakaoProperties

private const val kakaoApiKey = "test key"
private val kakaoMapsService = KakaoMapsService(KakaoProperties(kakaoApiKey))

val originalStationNames = listOf(
"압구정로데오","여의도","을지로입구","혜화","숙대입구(갈월)","을지로3가","종각","서초","신논현","충무로","삼성중앙","경복궁(정부서울청사)","상왕십리","종로5가","봉천","영등포시장","동대문역사문화공원","서대문","중랑","광화문(세종문화회관)","회현(남대문시장)","을지로3가","남영","명동","신도림","암사","종로3가","안국","불광","종로3가","시청","용산","신설동","합정","신촌","시청","고속터미널","충무로","이태원","청량리(서울시립대입구)","서울숲","사가정","동대입구","마포","문래","미아사거리","신촌","동대문","수유(강북구청)","영등포","한성대입구(삼선교)","독산","서울역","동대문역사문화공원","신설동","연신내","신설동","구로디지털단지","가산디지털단지","청량리(서울시립대입구)","신당","종로3가","한남","선릉","총신대입구(이수)","서울대입구(관악구청)","국회의사당","동대문","성수","약수","영등포구청","왕십리(성동구청)","왕십리(성동구청)","등촌","금천구청","화곡","논현","오류동","잠실새내","성신여대입구(돈암)","회기","신림","동묘앞","신사","약수","효창공원앞","연신내","삼각지","신당","구의(광진구청)","쌍문","군자(능동)","신림","가산디지털단지","성신여대입구(돈암)","까치산","압구정","디지털미디어시티","한티","언주","역삼","여의도","상봉(시외버스터미널)","공덕","뚝섬","낙성대","녹사평(용산구청)","군자(능동)","삼성(무역센터)","불광","아차산(어린이대공원후문)","신대방삼거리","천호(풍납토성)","건대입구","천호(풍납토성)","사당","안암(고대병원앞)","서울대벤처타운","가양","동묘앞","둔촌동","신논현","구로","홍대입구","선릉","구파발","노량진","석촌","대치","강남구청","신정네거리","공릉(서울과학기술대)","학동","디지털미디어시티","발산","명일","영등포구청","방배","건대입구","강동","신사","길동","공덕","망원","남부터미널(예술의전당)","강남","홍대입구","홍대입구","신도림","선정릉","양재(서초구청)","송정","먹골","송파","이대","상도","상봉(시외버스터미널)","신용산","당산","석계","노원","잠실(송파구청)","송파나루","당산","매봉","창동","선정릉","사당","강남","석촌","교대(법원.검찰청)","상수","강남구청","논현","노원","노량진","수서","잠실(송파구청)","양재(서초구청)","문정","고덕","교대(법원.검찰청)","서울역","가락시장","가락시장","강변(동서울터미널)","오목교(목동운동장앞)","마곡",
)
fun main() {
val lngLatResults = kakaoMapsService.findLngLat(originalStationNames.map {
if ("(" in it) {
it.split("(")[0]
} else {
it
} + ""
})
println(lngLatResults)
println(buildString {
appendLine("지하철역,lng,lat")
originalStationNames.zip(lngLatResults).forEach { (originalStationName, lngLatResult) ->
appendLine("$originalStationName,${lngLatResult.lng},${lngLatResult.lat}")
}
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.9.9
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import csv
import requests

# API endpoint and authorization token
API_URL = "https://api.staircrusher.club/admin/places/startCrawling"
AUTH_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJfYiI6IlwiYWRtaW5cIiIsImlzcyI6Im91ci1tYXAtc2VydmVyIiwiZXhwIjoxNzYwNTQzODIwfQ.IkHTLjaKS8iuhTCuLgCBQ-Y2ClxthrjV4opanjrIoJK5yFFCFPFL_bWl7N--MoEIIzeWq0w5pHdWA91QM-kGfQ"
HEADERS = {
"authorization": AUTH_TOKEN,
"content-type": "application/json"
}

# Function to calculate boundary vertices for 250m squares within the given area
def calculate_boundary_vertices(lng, lat, distance_km=1):
delta = distance_km / 111.32 # Approx. km per degree latitude
eighth_delta = delta / 8

# Define offsets for the 16 smaller 250m squares
offsets = [
(-3 * eighth_delta, -3 * eighth_delta), (-1 * eighth_delta, -3 * eighth_delta), (1 * eighth_delta, -3 * eighth_delta), (3 * eighth_delta, -3 * eighth_delta),
(-3 * eighth_delta, -1 * eighth_delta), (-1 * eighth_delta, -1 * eighth_delta), (1 * eighth_delta, -1 * eighth_delta), (3 * eighth_delta, -1 * eighth_delta),
(-3 * eighth_delta, 1 * eighth_delta), (-1 * eighth_delta, 1 * eighth_delta), (1 * eighth_delta, 1 * eighth_delta), (3 * eighth_delta, 1 * eighth_delta),
(-3 * eighth_delta, 3 * eighth_delta), (-1 * eighth_delta, 3 * eighth_delta), (1 * eighth_delta, 3 * eighth_delta), (3 * eighth_delta, 3 * eighth_delta),
]

# Generate boundary vertices for each square
squares = []
for offset_lng, offset_lat in offsets:
square = [
{"lng": lng + offset_lng - eighth_delta, "lat": lat + offset_lat - eighth_delta},
{"lng": lng + offset_lng + eighth_delta, "lat": lat + offset_lat - eighth_delta},
{"lng": lng + offset_lng + eighth_delta, "lat": lat + offset_lat + eighth_delta},
{"lng": lng + offset_lng - eighth_delta, "lat": lat + offset_lat + eighth_delta},
{"lng": lng + offset_lng - eighth_delta, "lat": lat + offset_lat - eighth_delta},
]
squares.append(square)
return squares

# Function to send a crawling request for a given set of boundary vertices
def send_crawling_request(boundary_vertices):
payload = {"boundaryVertices": boundary_vertices}
response = requests.post(API_URL, headers=HEADERS, json=payload)
if response.status_code == 200:
print("Crawling started successfully.")
else:
print(f"Failed to start crawling: {response.status_code}, {response.text}")

# Main function to process the CSV and initiate crawling
def main():
input_csv = "subway_stations.csv" # Replace with your CSV file name

with open(input_csv, newline='', encoding='utf-8') as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
station_name = row["지하철역"]
longitude = float(row["lng"])
latitude = float(row["lat"])

print(f"Processing station: {station_name} ({longitude}, {latitude})")

# Calculate boundary vertices for smaller squares
squares = calculate_boundary_vertices(longitude, latitude, 1) # 1 km as total area
for square in squares:
send_crawling_request(square)

if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import csv

def generate_chunked_insert_queries(csv_file, table_name, chunk_size=1000):
insert_queries = []
values = []
with open(csv_file, newline='', encoding='utf-8') as csvfile:
separator = ',\n'
reader = csv.DictReader(csvfile)
for i, row in enumerate(reader, start=1):
station_name = row['지하철역']
lng = float(row['lng'])
lat = float(row['lat'])

# Prepare value for the query
value = f"('{station_name}', {lng}, {lat}, ST_SetSRID(ST_MakePoint({lng}, {lat}), 4326))"
values.append(value)

# When chunk_size is reached, generate an INSERT query
if i % chunk_size == 0:
query = f"INSERT INTO {table_name} (name, lng, lat, geom) VALUES\n{separator.join(values)};"
insert_queries.append(query)
values = [] # Reset values for the next chunk

# Add remaining rows as the final query
if values:
query = f"INSERT INTO {table_name} (name, lng, lat, geom) VALUES\n{separator.join(values)};"
insert_queries.append(query)

return insert_queries

if __name__ == "__main__":
# Usage
csv_file = "subway_stations.csv" # Replace with your CSV file name
table_name = "data_subway_station" # Replace with your table name
queries = generate_chunked_insert_queries(csv_file, table_name, chunk_size=1000)

# Print queries
for query in queries:
print(query)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
지하철역,lng,lat
강남,127.02800140627488,37.49808633653005
석촌,127.107004062699,37.5054141216925
교대(법원.검찰청),127.013867969161,37.4927431676548
상수,126.92242989321456,37.54776618366393
강남구청,127.0413109462156,37.51721617197854
논현,127.02165424259898,37.51120000205266
노원,127.063449137455,37.6563403513278
노량진,126.940893179777,37.5135856714992
수서,127.104469788175,37.4852951142482
잠실(송파구청),127.10023101886318,37.51331105877401
양재(서초구청),127.03416413380752,37.48457681195669
문정,127.122484886901,37.4860310539381
고덕,127.15416500963447,37.55504766830918
교대(법원.검찰청),127.013867969161,37.4927431676548
서울역,126.96974961781686,37.55332892758497
가락시장,127.118262745146,37.4930992522183
가락시장,127.118262745146,37.4930992522183
강변(동서울터미널),127.094741101863,37.5351180385975
오목교(목동운동장앞),126.875307312696,37.5245340839144
마곡,126.82469691791624,37.56022863776301
Loading

0 comments on commit df95471

Please sign in to comment.