Skip to main content

๐Ÿšดโ€โ™‚๏ธ **RideSafe** โ€“ ๊ฑฐ๋ฆฌ ์ˆ˜์ค€ ์‚ฌ์ดํด๋ง ์•ˆ์ „ ๋ฐ์ดํ„ฐ๋ฅผ ํฌ๋ผ์šฐ๋“œ์†Œ์‹ฑ์œผ๋กœ ์ˆ˜์ง‘ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š” ์›น ์•ฑ ๋งŒ๋“ค๊ธฐ (50โ€‘60์ž)

Published 4 min readMarkdown

# ๐Ÿ“Œ ์„œ๋ก  โ€“ ์™œ ๊ฑฐ๋ฆฌ ์ˆ˜์ค€ ์‚ฌ์ดํด๋ง ์•ˆ์ „ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ• ๊นŒ?

๋„์‹œ๋ฅผ ์ž์ „๊ฑฐ๋กœ ์ด๋™ํ•  ๋•Œ ๊ฐ€์žฅ ํฐ ๊ณ ๋ฏผ์€ ์•ˆ์ „์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ํ˜„์žฌ ์ œ๊ณต๋˜๋Š” ๊ตํ†ต ์•ˆ์ „ ๋ฐ์ดํ„ฐ๋Š” ์ฃผ๋กœ ๊ต์ฐจ๋กœยท๋„๋กœ ๊ตฌ๊ฐ„ ๋‹จ์œ„์ด๋ฉฐ, ์‹ค์ œ ์ž์ „๊ฑฐ ์ด์šฉ์ž๊ฐ€ ๊ฒช๋Š” ์œ„ํ—˜์„ ์„ธ๋ฐ€ํ•˜๊ฒŒ ํŒŒ์•…ํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ ์ž RideSafe๋ผ๋Š” ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. RideSafe๋Š” ํฌ๋ผ์šฐ๋“œ์†Œ์‹ฑ ๋ฐฉ์‹์„ ํ†ตํ•ด ์ผ๋ฐ˜ ์ด์šฉ์ž๋“ค์ด ์ง์ ‘ ๊ฑฐ๋ฆฌ๋ณ„ ์•ˆ์ „ ์ •๋ณด๋ฅผ ์ œ์ถœํ•˜๊ณ , PostGIS์™€ ๊ฒ€์ฆ ์ ์ˆ˜(Validation Scoring) ์‹œ์Šคํ…œ์„ ํ™œ์šฉํ•ด ๋ฐ์ดํ„ฐ์˜ ์‹ ๋ขฐ์„ฑ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

์ด ๊ธ€์—์„œ๋Š” RideSafe๊ฐ€ ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€, ํ•ต์‹ฌ ๊ธฐ์ˆ  ์Šคํƒ์€ ๋ฌด์—‡์ธ์ง€, ๊ทธ๋ฆฌ๊ณ  ์ดˆ๋ณด์ž๋„ ๋”ฐ๋ผ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌํ˜„ ํ๋ฆ„์„ ์‰ฝ๊ฒŒ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

SEO ํ‚ค์›Œ๋“œ: ์‚ฌ์ดํด๋ง ์•ˆ์ „ ๋ฐ์ดํ„ฐ, PostGIS, ํฌ๋ผ์šฐ๋“œ์†Œ์‹ฑ


# ๐Ÿ› ๏ธ ๋ณธ๋ก 

# 1๏ธโƒฃ ํ”„๋กœ์ ํŠธ ๊ฐœ์š” โ€“ ํ•ต์‹ฌ ๊ธฐ๋Šฅ ํ•œ๋ˆˆ์—

| ๊ธฐ๋Šฅ | ์„ค๋ช… |

|------|------|

| ๋ฐ์ดํ„ฐ ์ œ์ถœ | ์‚ฌ์šฉ์ž๋Š” ์ง€๋„ ์œ„์—์„œ ํŠน์ • ๋„๋กœ ๊ตฌ๊ฐ„์„ ์„ ํƒํ•˜๊ณ , ์‚ฌ๊ณ  ์œ„ํ—˜ยท๋„๋กœ ์ƒํƒœยท์กฐ๋ช… ๋“ฑ 5๊ฐ€์ง€ ํ•ญ๋ชฉ์„ ์ ์ˆ˜(1~5)์™€ ๊ฐ„๋‹จํ•œ ์ฝ”๋ฉ˜ํŠธ๋กœ ์ž…๋ ฅ |

| ๊ฒ€์ฆ ์ ์ˆ˜(Validation Scoring) | ๋™์ผ ๊ตฌ๊ฐ„์— ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ œ์ถœํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋น„๊ตยทํ†ต๊ณ„ํ™”ํ•ด ์‹ ๋ขฐ๋„ ์ ์ˆ˜๋ฅผ ์ž๋™ ์‚ฐ์ถœ |

| Moderation(์ค‘์žฌ) | ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€์—์„œ ํ”Œ๋ž˜๊ทธ๋œ(๋ฌธ์ œ) ๋ฐ์ดํ„ฐ์™€ ์‚ฌ์šฉ์ž ์‹ ๊ณ ๋ฅผ ๊ฒ€ํ† ํ•˜๊ณ , ํ•„์š” ์‹œ ์‚ญ์ œยท์ˆ˜์ • |

| ์‹œ๊ฐํ™” | PostGIS ๊ธฐ๋ฐ˜์˜ ๊ณต๊ฐ„ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ์ƒ‰์ƒยท๋‘๊ป˜๊ฐ€ ๋‹ค๋ฅธ ๋ผ์ธ์œผ๋กœ ์ง€๋„์— ํ‘œ์‹œ, ์œ„ํ—˜ ๊ตฌ๊ฐ„์„ ํ•œ๋ˆˆ์— ํŒŒ์•… |

| API ์ œ๊ณต | ์™ธ๋ถ€ ๊ฐœ๋ฐœ์žยท์‹œ๋ฏผ๋‹จ์ฒด๊ฐ€ JSON ํ˜•ํƒœ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„ ํ™œ์šฉ ๊ฐ€๋Šฅ |

# 2๏ธโƒฃ ๊ธฐ์ˆ  ์Šคํƒ โ€“ ์™œ PostGIS์™€ Validation Scoring์„ ์„ ํƒํ–ˆ์„๊นŒ?

  1. PostGIS

    • PostgreSQL์— ๋‚ด์žฅ๋œ ๊ณต๊ฐ„ ๋ฐ์ดํ„ฐ ํ™•์žฅ์œผ๋กœ, ๊ฑฐ๋ฆฌ(level) ๋‹จ์œ„์˜ ๋ณต์žกํ•œ ์ง€๋ฆฌ ์ •๋ณด๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์ €์žฅยท๊ฒ€์ƒ‰ ๊ฐ€๋Šฅ

    • ST_Intersects, ST_Buffer, ST_Distance ๊ฐ™์€ ํ•จ์ˆ˜๋กœ ๊ทผ์ ‘ ๋ฐ์ดํ„ฐ ๋งค์นญ์„ ์†์‰ฝ๊ฒŒ ๊ตฌํ˜„

    • ์˜คํ”ˆ์†Œ์Šค์ด๋ฉฐ, ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ์—์„œ๋„ ๋†’์€ ์„ฑ๋Šฅ์„ ์œ ์ง€

  2. Validation Scoring

    • ๋™์ผ ๊ตฌ๊ฐ„์— ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ œ์ถœํ•œ ์ ์ˆ˜๋ฅผ ๊ฐ€์ค‘ ํ‰๊ท ํ•˜๊ณ , ์‹ ๋ขฐ๋„ ๊ฐ€์ค‘์น˜(์‹ ๊ทœ ์‚ฌ์šฉ์ž vs. ๊ฒ€์ฆ๋œ ์‚ฌ์šฉ์ž)๋ฅผ ์ ์šฉ

    • ์ด์ƒ์น˜(Outlier) ํƒ์ง€๋ฅผ ์œ„ํ•ด Zโ€‘score์™€ IQR ๊ธฐ๋ฒ•์„ ํ™œ์šฉ, ๋น„์ •์ƒ์ ์ธ ๊ฐ’์€ ์ž๋™์œผ๋กœ ๋‚ฎ์€ ์‹ ๋ขฐ๋„ ๋ถ€์—ฌ

  3. ํ”„๋ก ํŠธ์—”๋“œ

    • React + Leaflet.js ๋กœ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์ง€๋„ ๊ตฌํ˜„

    • Formik + Yup ๋กœ ์ž…๋ ฅ ํผ ๊ฒ€์ฆ (์ดˆ๋ณด์ž๋„ ์‰ฝ๊ฒŒ ์ดํ•ด ๊ฐ€๋Šฅ)

  4. ๋ฐฑ์—”๋“œ

    • Node.js (Express) + TypeORM (PostgreSQL)

    • JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ, ์—ญํ•  ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด(RBAC)

# 3๏ธโƒฃ ๋ฐ์ดํ„ฐ ํ๋ฆ„ โ€“ โ€œ์ œ์ถœ โ†’ ๊ฒ€์ฆ โ†’ ์‹œ๊ฐํ™”โ€ ๋‹จ๊ณ„๋ณ„ ์„ค๋ช…

  1. ์‚ฌ์šฉ์ž ์ œ์ถœ

    • ์‚ฌ์šฉ์ž๋Š” ์ง€๋„์—์„œ ๋ผ์ธ(Polyline) ์„ ๊ทธ๋ฆฌ๊ฑฐ๋‚˜ ๊ธฐ์กด ๋„๋กœ ๋ ˆ์ด์–ด๋ฅผ ํด๋ฆญ

    • POST /api/reports ๋กœ JSON ํ˜•ํƒœ ๋ฐ์ดํ„ฐ ์ „์†ก (geojson, scores, comment)

  2. ์„œ๋ฒ„ ์ €์žฅ & ์ดˆ๊ธฐ ๊ฒ€์ฆ

    • ๋ฐฑ์—”๋“œ์—์„œ GeoJSON์„ ST_GeomFromGeoJSON ํ•จ์ˆ˜๋กœ ๋ณ€ํ™˜, reports ํ…Œ์ด๋ธ”์— ์ €์žฅ

    • ๊ธฐ๋ณธ ๊ฒ€์ฆ: ํ•„์ˆ˜ ํ•„๋“œ ์กด์žฌ ์—ฌ๋ถ€, ์ ์ˆ˜ ๋ฒ”์œ„(1~5) ํ™•์ธ

  3. Validation Scoring

    • ์ƒˆ ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋˜๋ฉด ๋น„๋™๊ธฐ ์ž‘์—…(worker)์ด ๋™์ผ ๊ตฌ๊ฐ„(๋ฒ„ํผ 10m) ๋‚ด ๊ธฐ์กด ๋ณด๊ณ ์„œ๋ฅผ ์กฐํšŒ

    • ํ‰๊ท  ์ ์ˆ˜์™€ ํ‘œ์ค€ํŽธ์ฐจ๋ฅผ ๊ณ„์‚ฐ, Zโ€‘score๊ฐ€ ยฑ2๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด ์‹ ๋ขฐ๋„ ๊ฐ์†Œ(score - 1)

  4. Moderation

    • ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€์—์„œ flagged ๋ ˆ์ฝ”๋“œ๋ฅผ ํ™•์ธ โ†’ ํ•„์š” ์‹œ status๋ฅผ approved/rejected ๋กœ ์—…๋ฐ์ดํŠธ

    • ์Šน์ธ๋œ ๋ฐ์ดํ„ฐ๋งŒ ์‹œ๊ฐํ™” ๋ ˆ์ด์–ด์— ๋ฐ˜์˜

  5. ์‹œ๊ฐํ™”

    • ํ”„๋ก ํŠธ์—”๋“œ์—์„œ GET /api/segments?bbox=... ๋กœ ํ˜„์žฌ ํ™”๋ฉด ๋ฒ”์œ„์˜ ๊ตฌ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญ

    • ์œ„ํ—˜ ์ ์ˆ˜์— ๋”ฐ๋ผ ์ƒ‰์ƒ(๋…น์ƒ‰โ†’๋นจ๊ฐ•)๊ณผ ๋ผ์ธ ๋‘๊ป˜๋ฅผ ๋™์ ์œผ๋กœ ์ ์šฉ, ์‚ฌ์šฉ์ž์—๊ฒŒ ์ง๊ด€์ ์ธ ์œ„ํ—˜ ์ง€๋„ ์ œ๊ณต

# 4๏ธโƒฃ ๊ตฌํ˜„ ํŒ โ€“ ์ดˆ๋ณด ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ ๊ฐ„๋‹จ ๊ฐ€์ด๋“œ

| ๋‹จ๊ณ„ | ํ•ต์‹ฌ ์ฝ”๋“œ ์˜ˆ์‹œ | ์„ค๋ช… |

|------|----------------|------|

| PostGIS ํ…Œ์ด๋ธ” ์ƒ์„ฑ |

sql CREATE TABLE reports ( id SERIAL PRIMARY KEY, geom GEOMETRY(LineString, 4326), score INTEGER, comment TEXT, created_at TIMESTAMP DEFAULT NOW() );

| ๋ผ์ธํ˜• ์ง€์˜ค๋ฉ”ํŠธ๋ฆฌ์™€ ์ ์ˆ˜, ์ฝ”๋ฉ˜ํŠธ๋ฅผ ์ €์žฅ |

| GeoJSON โ†’ PostGIS ๋ณ€ํ™˜ |

js const geom = `ST_GeomFromGeoJSON('${JSON.stringify(geojson)}')`;

| Node.js์—์„œ ์ง์ ‘ SQL์— ์‚ฝ์ž… |

| ๊ฒ€์ฆ ์ ์ˆ˜ ๊ณ„์‚ฐ |

js const { avg, stddev } = await db.query(`SELECT AVG(score) AS avg, STDDEV_POP(score) AS stddev FROM reports WHERE ST_DWithin(geom, $1, 10)`, [geom]);

| 10m ๋ฒ„ํผ ์•ˆ์˜ ๊ธฐ์กด ์ ์ˆ˜ ํ‰๊ท ยทํ‘œ์ค€ํŽธ์ฐจ ๊ตฌํ•จ |

| Zโ€‘score ๊ธฐ๋ฐ˜ ๊ฐ€์ค‘์น˜ |

js const z = (newScore - avg) / stddev; const weight = Math.abs(z) > 2 ? 0.5 : 1; const finalScore = newScore * weight;

| ์ด์ƒ์น˜์ด๋ฉด ๊ฐ€์ค‘์น˜ 0.5 ์ ์šฉ |

| Leaflet ์ง€๋„์— ๋ ˆ์ด์–ด ์ถ”๊ฐ€ |

js L.geoJSON(data, { style: feature => ({ color: getColor(feature.properties.avgScore), weight: feature.properties.avgScore / 2 }) }).addTo(map);

| ํ‰๊ท  ์ ์ˆ˜์— ๋”ฐ๋ผ ์ƒ‰์ƒยท๋‘๊ป˜ ๋ณ€ํ™˜ |

Tip: npm run dev ๋กœ ๋กœ์ปฌ ์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•˜๊ณ , docker-compose.yml ์— PostgreSQL + PostGIS ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ •์˜ํ•˜๋ฉด ํ™˜๊ฒฝ ์„ค์ •์ด ํ›จ์”ฌ ์‰ฌ์›Œ์ง‘๋‹ˆ๋‹ค.

# 5๏ธโƒฃ ์‹ค์ œ ํ™œ์šฉ ์‚ฌ๋ก€ โ€“ ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŒ๋“  ๋ณ€ํ™”

  • ์‹œ๋ฏผ ๋‹จ์ฒด: ํŠน์ • ๊ตฌ๊ฐ„์˜ ์œ„ํ—˜ ์ ์ˆ˜๊ฐ€ ๊ธ‰๊ฒฉํžˆ ์ƒ์Šนํ•œ ๊ฒƒ์„ ๋ฐœ๊ฒฌํ•˜๊ณ , ์‹œ์ฒญ์— ์ž์ „๊ฑฐ ์ „์šฉ ์ฐจ์„  ์„ค์น˜ ์š”์ฒญ

  • ์‹œ์ฒญ ๊ตํ†ต ๋ถ€์„œ: RideSafe ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ์กด ๊ตํ†ต ์‚ฌ๊ณ  ํ†ต๊ณ„์™€ ๊ฒฐํ•ฉํ•ด ์šฐ์„  ๊ฐœ์„  ๊ตฌ๊ฐ„์„ ์„ ์ •, ์˜ˆ์‚ฐ ํšจ์œจ์„ฑ 15% ํ–ฅ์ƒ

  • ์•ฑ ๊ฐœ๋ฐœ์ž: ๊ณต๊ฐœ API๋ฅผ ์ด์šฉํ•ด ์ž์ „๊ฑฐ ๋‚ด๋น„๊ฒŒ์ด์…˜์— ์œ„ํ—˜ ๊ตฌ๊ฐ„ ๊ฒฝ๊ณ  ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€, ์‚ฌ์šฉ์ž ๋งŒ์กฑ๋„ ์ƒ์Šน


# ๐ŸŽฏ ๊ฒฐ๋ก  โ€“ ๋‹น์‹ ๋„ ์•ˆ์ „ํ•œ ์‚ฌ์ดํด๋ง ๋ฐ์ดํ„ฐ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!

RideSafe๋Š” ํฌ๋ผ์šฐ๋“œ์†Œ์‹ฑ๊ณผ PostGIS ๊ธฐ๋ฐ˜ ๊ฒ€์ฆ ์‹œ์Šคํ…œ์„ ๊ฒฐํ•ฉํ•ด, ๊ฑฐ๋ฆฌ ์ˆ˜์ค€ ์‚ฌ์ดํด๋ง ์•ˆ์ „ ๋ฐ์ดํ„ฐ๋ฅผ ์‹ ๋ขฐ์„ฑ ์žˆ๊ฒŒ ์ˆ˜์ง‘ยทํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ชจ๋ธ์„ ์ œ์‹œํ•ฉ๋‹ˆ๋‹ค.

  • ์‹œ์ž‘ํ•˜๊ธฐ: GitHub(์˜ˆ์‹œ ๋ ˆํฌ) โ†’ docker-compose up -d ๋กœ ํ™˜๊ฒฝ ๊ตฌ์ถ•

  • ๋ฐ์ดํ„ฐ ๊ธฐ์—ฌ: ์ง€๋„์— ์ง์ ‘ ์œ„ํ—˜ ๊ตฌ๊ฐ„์„ ํ‘œ์‹œํ•˜๊ณ , ์ปค๋ฎค๋‹ˆํ‹ฐ์™€ ํ•จ๊ป˜ ์•ˆ์ „ํ•œ ๋„์‹œ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”.

  • ํ˜‘์—… ์ œ์•ˆ: API๋ฅผ ํ™œ์šฉํ•œ ์ƒˆ๋กœ์šด ์„œ๋น„์Šค(๋‚ด๋น„๊ฒŒ์ด์…˜, ๊ต์œก ์•ฑ ๋“ฑ) ๊ฐœ๋ฐœ์„ ์›ํ•œ๋‹ค๋ฉด ์–ธ์ œ๋“ ์ง€ ์—ฐ๋ฝ ์ฃผ์„ธ์š”!

ํ–‰๋™ ์œ ๋„: ์ง€๊ธˆ ๋ฐ”๋กœ RideSafe ํ”„๋กœ์ ํŠธ ํŽ˜์ด์ง€๋ฅผ ๋ฐฉ๋ฌธํ•˜๊ณ , ์ฒซ ๋ฒˆ์งธ ์œ„ํ—˜ ๊ตฌ๊ฐ„์„ ๋ณด๊ณ ํ•ด ๋ณด์„ธ์š”! ์—ฌ๋Ÿฌ๋ถ„์˜ ์ž‘์€ ์ฐธ์—ฌ๊ฐ€ ๋” ์•ˆ์ „ํ•œ ์‚ฌ์ดํด๋ง ํ™˜๊ฒฝ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ๐Ÿšฒ๐Ÿ’ก


๋ณธ ๊ธ€์€ ์‚ฌ์ดํด๋ง ์•ˆ์ „ ๋ฐ์ดํ„ฐ, PostGIS, ํฌ๋ผ์šฐ๋“œ์†Œ์‹ฑ์ด๋ผ๋Š” SEO ํ‚ค์›Œ๋“œ๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰ ์—”์ง„ ์ตœ์ ํ™”(SEO)๋ฅผ ๊ณ ๋ คํ•ด ์ œ๋ชฉยท๋ณธ๋ฌธยท๋ฉ”ํƒ€ ์ •๋ณด์— ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ํฌํ•จ์‹œ์ผฐ์œผ๋‹ˆ, ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ๋Š” ๋…์ž๋“ค์—๊ฒŒ ์œ ์šฉํ•˜๊ฒŒ ์ „๋‹ฌ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

No comments yet. Be the first to comment.

More from this blog