Repository navigation
Design Documentation
Team 9, Bonnie and Claude · Software Development Principles and Practice, Fall 2026 · Version 1.0 (Iteration 1, 2026-10-09)
This page is for the development team: how the system is built, why, and what comes next. It describes the system as built and run in Iteration 1. Words with a capital letter, such as User, Quest and Party, are the terms of the Requirements and Specifications page (its section 5.1); on screen a Quest is a "파티" and a Party is the "활성 파티". Values marked provisional were chosen to make the prototype run and are expected to change.
Contents
- Document Revision History
- System Architecture: architecture, mobile app, patterns, libraries, decisions, main flows
- Design Details: data model, where data lives, API, messages, visibility rule, algorithms, implementation decisions
- Prototype Findings, Design Changes and the Next Iteration
- Design Patterns (Iteration 5)
| Version | Date | Key changes |
|---|---|---|
| 0.1 | 2026-09-28 | Initial draft, written from the specs before implementation. |
| 1.0 | 2026-10-09 | Rewritten to match the Iteration 1 prototype. Servers call each other over HTTP, and only fan-out uses Redis events. The Quest carries recruiting and the Party is the group together now. Places come from open data with ids computed from their source. Sessions, Administrators and Friend IDs added. Data model, API and messages replaced by the implemented ones. Section 4 (findings, design changes, next iteration) written. |
Two clients, four servers and two data stores. The mobile app talks to one API, the main server, and keeps one socket connection to the socket server. A call with one receiver goes over HTTP with a shared secret; a signal that every socket server must deliver goes over Redis as an event. Each database belongs to one server.

Thick arrows are the clients' two connections, thin arrows are calls between servers and to stores, and the dotted arrow is the one Redis channel. The two databases are in one PostgreSQL instance with two roles. In Iteration 1 every server runs on one computer with Docker Compose; a phone on the same network reaches ports 3000 and 3001 directly, and any other build goes through an https tunnel.
| Component | Responsibility | Owns |
|---|---|---|
| Mobile app | The map, Avatars and every screen. Uploads the User's position in front and in the background. Fetches an item again when a signal names it. | Tokens in secure storage and a few preferences. |
| Admin site | Reviews and publishes Drafts with a position picker on a map; creates, edits and cancels events; shows Places, Collection status, Users and Administrators. | Nothing. The token is an HttpOnly cookie. |
| main-server | The only API: sign-in and sessions, Friends, Meetups, Quests, Parties, Location Sharing and the visibility rule, timetable, Global Events, Places, menus, shuttle, walking route. Receives collected data and Matching results, and publishes signals. | The main database; positions, vehicles and idempotency records in Redis; the token signing key. |
| socket-server | Verifies the token on connect, keeps a room per User and per session, and delivers positions, signals, session endings and shuttle vehicles. | Nothing. |
| match-server | Holds Matching requests and runs the rounds (section 3.6). | The match database. |
| worker-server | Collects menus, events, shuttle stops and vehicles on a schedule and hands them to the main server; reports failures. | Nothing. |

No screen calls the native map, the socket or the network directly. The map component is the only part that names a map provider: the Android module is a local Expo module in Kotlin around the Kakao Maps SDK, and the iOS module is the same interface in Swift. In Expo Go and in tests a plain ground stands in for the map. Server state lives in a query cache: a signal invalidates the keys it names, and a pushed position replaces one entry.
- Client-server. The app and the admin site are clients of the main server, so who may see whom is enforced in one place that a phone cannot change.
-
Microservices with one database per service. main-server owns
mainand match-server ownsmatch. Ids are copied without foreign keys, so a change in one server's tables cannot break another. - HTTP for one receiver, publish and subscribe for fan-out. A call that needs an answer is simplest over HTTP; an event that every socket server must see is simplest over a channel.
- Signal and fetch. A change signal carries no content and the app fetches the item again, so authorisation stays in the API and a lost signal only delays a screen update. Only positions and shuttle vehicles are pushed with coordinates.
-
Adapter in front of a provider. The map component hides the map SDK, and
Groupingin the match server hides the grouping rule. Either can be replaced by changing one module.
| Area | Libraries and versions | Used for |
|---|---|---|
| All projects | Node.js 24, pnpm 12.6, TypeScript 6.0, oxlint 1.58, Prettier 3 | Runtime, one lockfile per project, strict typing, one lint and format configuration. |
| Servers | NestJS 12.1 with microservices, websockets (Socket.IO 4.8), schedule, jwt and idempotency 0.0.1 (pinned); Zod 4.6; google-auth-library 11.1; ioredis 6.0; cheerio 1.2 |
The four servers: Redis transport, sockets, scheduled Collections and Matching rounds, ES256 tokens, repeatable creating requests, validation, ID token verification, HTML parsing in the worker. |
| Data | PostgreSQL 18, Prisma 7.10.0 (pinned), Redis 8 | Two databases in one instance, migrations with partial indexes and CHECK constraints; events, positions, vehicles and idempotency records. PostGIS 3 is installed and unused. |
| Mobile app | Expo SDK 57, React Native 0.86 (New Architecture), Expo Router, TanStack Query 5, expo-location, expo-task-manager, expo-secure-store, socket.io-client 4.8, react-native-nitro-google-signin 2.3 |
Screens, the query cache, position sending in front and in the background, token storage, the socket, Sign in with Google through Android Credential Manager. |
| Map | Kakao Maps SDK (Android 2.15, iOS 2.12), Kakao walking route REST API | The map, through the team's native modules (ARM devices only); walking routes, 1,000 free calls a day. |
| Admin site | Next.js 16.3, React 19.2, Tailwind 4, Google Identity Services, Kakao Maps JavaScript SDK | The admin site and its position picker. |
| Tests | Vitest 4.1, Testcontainers 12.1, Jest (jest-expo) | Server and admin tests against real PostgreSQL and Redis containers; app tests. |
| Data sources | The SNU event list; the Co-op, dormitory and veterinary college menu pages; the shuttle operator's route page and vehicle endpoint. Seed data from the SNU campus map, OpenStreetMap and 국토지리정보원. | Collected by the worker; seed data is loaded at start. |
No AI model or language-model service is used in Iteration 1. Section 4.4 plans the first one.
Each decision names what was chosen, why, and what was not chosen.
- Six independent projects, no shared package. Message shapes are written twice and validated by the receiver. Independence of dependencies, deployment and data was put above sharing code. Not chosen: a monorepo with a shared types package.
- HTTP between servers; Redis events only from the main server to the socket servers. Each call between servers has one receiver and needs an answer and a timeout, and the worker and match server then need no Redis. Not chosen: request and response over Redis for everything.
- A signal may be lost; the one message that must not be lost is repeated. A lost signal only delays a screen, because the app fetches on connect, on reconnect and on return to the front. Creating a match's Shared Quest is an HTTP call repeated every round and handled once by its match id. Not chosen: a transactional outbox or a durable queue, to be reconsidered when notifications arrive.
- Positions and shuttle vehicles are pushed with coordinates; everything else is signal and fetch. Avatars must move within seconds, while everything else changes slowly. The app never polls.
- Rules that must hold under concurrent requests are enforced by PostgreSQL: partial unique indexes, CHECK constraints and row locks. Two requests can both pass a check in code before either writes. Not chosen: rules in code only.
- Only the latest position is kept, in Redis, for 10 minutes. Only the newest position matters, no history is the more private choice, and state that is replaced every few seconds does not belong in a table. Shuttle vehicles are kept the same way for 60 seconds.
- The Campus Boundary and the Place at a point are computed in server code, not with PostGIS. One boundary and 230 outlines need no spatial index, and one answer takes about 50 µs.
- Reference data comes from open sources and keeps its ids. Places come from the SNU campus map (224), OpenStreetMap (2) and the national map (4), outlines from 국토지리정보원, and a Place's id is the UUID v5 of its source id. Korean map services forbid storing their data, OpenStreetMap had too few outlines, and seed data reloaded at every start must keep its ids (ADR 0002).
-
Sign in with Google through Credential Manager, restricted to
snu.ac.kr, with one session per User. The server verifies the ID token's signature, audience, expiry, verified e-mail and hosted domain. The access token lasts 1 hour and is signed with ES256, so the socket server verifies it with the public key alone; the refresh token lasts 30 days and rotates. Sessions are a table read on every request, so signing in elsewhere or signing out takes effect at once. Administrators are a separate table with their own Google client. Not chosen: the legacy Google SDK (removed in 2026-08), a shared signing secret, sessions in Redis, Administrators as a flag on the User. - The Quest carries recruiting; the Party is the group together now. The Quest holds the Leader, capacity, Join Policy, Board and members. A Party is opened by hand, one per User and one running per Quest. People recruit days ahead and share location only while together. Not chosen: recruiting on the Party, or one object for both.
- Matching runs in rounds and produces a Quest, never a Party. Every 60 seconds (provisional) one match server, under an advisory lock, first places requesters into Open Quests and then groups the rest by shared hashtags. Rounds keep several match servers correct, and an Open Quest with a free place beats a new group.
- Collected text is kept as written when the parser is unsure. A meal is the lines of the page's cell, priced only when certain: only 114 of the first 303 lines were a dish with a price (ADR 0001). A notice is published by rules only when its start time and exactly one Place are certain, and is otherwise a Draft with the original text: 13 of the first 82 posts were published.
Sign-in. The app obtains a Google ID token through Credential Manager and posts it. The main server verifies it, creates the User with a Friend ID on the first sign-in, ends a session that is open on another phone, and answers an access token and a refresh token. The socket connects with the access token and joins the User's and the session's rooms. After an hour a 401 triggers one refresh and one retry. A session ended elsewhere answers SESSION_REPLACED, and the app shows "다른 기기에서 로그인했어요".
Matching creates a Quest. User B asks in the same way as User A.
A Party, and positions on the map.
An Invite Link and a Meetup.
Campus services. The worker posts each Collection to the main server, which stores it and serves it to the app. Shuttle vehicles are pushed over the socket every 15 seconds.
Every identifier is a UUID generated by the database, except a Place's id (decision 8). The main database has 25 tables after 16 migrations, and the match database has 2.

| Tables | Key columns | Notes |
|---|---|---|
users |
google_subject, name, department, hashtags, friend_id, master_switch_on
|
The Master Switch is off by default. |
sessions, refresh_tokens
|
user_id, ended_at; the token's hash |
One open session per User. |
friendships |
user_a_id, user_b_id, accepted_at, user_a_sharing, user_b_sharing
|
A null accepted_at is a Friend request. One sharing switch per side. |
invite_links |
sender_id, token_hash, expires_at, used_at
|
Valid 24 hours, used once. |
meetups |
proposer_id, receiver_id, starts_at, place_id or a point, state
|
proposed, accepted, declined or withdrawn. |
global_events |
title, starts_at, place, latitude, longitude, state, version, post_number
|
draft, published, cancelled or discarded. |
quests |
leader_id, global_event_id, capacity, join_policy, board, description, match_id
|
Capacity 1 to 8. board is null exactly when the policy is closed. |
quest_holders, sub_quests, sub_quest_progress
|
quest_id, user_id, joined_at; starts_at, place_id; done_at
|
Members, steps ("일정") and each member's progress. |
parties, party_members
|
quest_id, leader_id; user_id, sharing
|
One running Party per Quest and one Party per User. |
quest_join_requests, quest_invitations, and the same two for parties |
quest_id or party_id, user_id
|
One row per Quest or Party and User. |
timetable_classes, class_times
|
course_name; weekday, start_time, end_time, place_id, room
|
1 to 10 times per class. |
places |
origin, number, name, outlines
|
230 Places. |
restaurant_days, menu_lines
|
the line as written; kind, name and price when sure | Decision 12. |
shuttle_routes, shuttle_stops, collection_statuses, administrators
|
the route line and its stops; last success and failure per source; e-mail | |
matching_requests, matches (database match) |
User, event, size, copied hashtags, state; event, state, Quest id | Ids of the main database, copied without foreign keys. |
Rules the database enforces, so that they hold when two requests arrive at the same moment:
| Rule | How it is enforced |
|---|---|
| One Party per User, one running Party per Quest, one open session per User, one waiting Matching request per User and event | Unique or partial unique indexes |
| A Quest or Party never exceeds its capacity | The row is locked and the count checked inside the transaction |
| One Quest per Global Event per User | Unique (user_id, global_event_id) in quest_holders
|
| An Invite Link is used once; a match creates one Shared Quest | A conditional update of used_at in the friendship's transaction; a unique match_id on quests
|
| A Quest recruits in public only with a Board | CHECK (board IS NULL) = (join_policy = 'closed')
|
| Two Administrators do not overwrite each other |
version compared in the write |
| Only one match server runs a round | A transaction-scoped advisory lock |
| Data | Where | Kept for |
|---|---|---|
| Users, sessions, friendships, Invite Links, Meetups, Quests, Parties, Global Events, timetables, menus | PostgreSQL main
|
Until removed or replaced |
| Matching requests and matches | PostgreSQL match
|
Until removed |
| Places, outlines, the shuttle route and stops, the Campus Boundary | Seed files, loaded at start | Permanent |
| Latest position per User | Redis, position:<userId>
|
10 minutes; cleared at once when the switch goes off, the position is off campus or the session ends |
| Latest shuttle vehicles; idempotency records | Redis | 60 seconds; 24 hours |
| Tokens; consent and the background choice | The phone's secure storage; app storage | Until sign-out; until cleared |
| Location history, walking routes, Class Quests | Never stored | Routes are answered no-store; Class Quests are computed from the timetable when read |
Every route is served by the main server as JSON over HTTP, with the access token in the Authorization header. A creating request without a rule of its own requires an Idempotency-Key. Access: U a signed-in User, A an Administrator, W the worker's token, M the match server's token, P public.
| Area (access) | Main routes |
|---|---|
| Sign-in (P, U) |
POST /auth/google, POST /auth/refresh, POST /auth/sign-out, POST /lobby
|
| Profile (U) |
GET /users/me, POST /users/me/onboarding, PATCH /users/me/profile, PUT /users/me/master-switch
|
| Location (U) |
POST /positions (answers whether off campus), GET /positions (what the viewer may see now) |
| Friends (U) |
GET /friend-ids/{id}, POST /friend-requests, POST /friend-requests/{id}/accept, GET /friends, DELETE /friends/{userId}, PUT /friends/{userId}/sharing, POST /invite-links, POST /invite-links/{token}/accept
|
| Meetups (U) |
POST /meetups, GET /meetups, POST /meetups/{id}/accept (also decline, withdraw) |
| Quests (U) |
POST /quests (attend an event), POST /quests/own, GET /quests (with today's Class Quests), GET /quests/recruiting, PATCH /quests/{id}, POST /quests/{id}/join, PUT /quests/{id}/leader, POST /quests/{id}/end; steps under /quests/{id}/sub-quests; join requests and invitations under /quests/{id}/join-requests and /quests/{id}/invitations
|
| Matching (U) |
POST /matching-requests, GET /matching-requests, POST /matching-requests/{eventId}/withdraw
|
| Parties (U) |
POST /parties, GET /parties/mine, POST /parties/{id}/join, PUT /parties/mine/sharing, POST /parties/mine/leave, POST /parties/mine/end
|
| Campus data (U) |
GET /timetable/classes (also POST, PUT, DELETE), GET /places, GET /places/search, GET /places/at, GET /global-events, GET /menus, GET /shuttle, GET /shuttle/vehicles, GET /walking-route
|
| Administrative (A) |
POST /admin/auth/google, GET /admin/global-events, PATCH /admin/global-events/{id}, POST /admin/global-events/{id}/publish (also discard, cancel), GET /admin/places, GET /admin/collection-statuses, GET /admin/users, GET /admin/administrators
|
| Collection (W) |
POST /menus/collected, POST /global-events/collected, POST /shuttle/stops/collected, POST /shuttle/vehicles/collected, POST /collections/failed
|
| Matching rounds (M) |
POST /matching-requests/standing, POST /matching-requests/eligible-quests, POST /matching-requests/placements, POST /matches/{id}/quest
|
A refusal carries a code such as QUEST_FULL, PARTY_EXISTS_FOR_QUEST, MASTER_SWITCH_OFF or INVITE_LINK_USED. The app keeps one table that turns each code into Korean.
| Socket event | Carries | Sent to | The app |
|---|---|---|---|
position, position-removed
|
User id, coordinates and time; User id | The viewer's room | Glides the Avatar; removes it at once |
quests-changed, party-changed, matching-changed, meetups-changed, friends-changed
|
Nothing | The User's room | Fetches the named list again |
global-events-changed, shuttle-vehicles-updated
|
Nothing; the vehicles | Everyone | Fetches events again; replaces the vehicles while the layer is on |
session-ended |
The reason | The session's room | Signs out, with "다른 기기에서 로그인했어요" when replaced |
Between servers every receiver validates the message against a schema. The worker's posts are answered, and the next scheduled Collection repeats lost data.
One service answers "may this viewer see this subject now?", and every push and every read of positions asks it:
- No, if the viewer's or the subject's Master Switch is off.
- No, if the subject has no position newer than 10 minutes, or is outside the Campus Boundary.
- Yes, if a friendship or a common Party exists with its switch on at both ends.
- Otherwise no. Holding the same Quest is not a relationship.
So one active relationship is enough, sharing is always mutual, and "hidden", "switched off" and "unrelated" look the same to a viewer. Any change that ends visibility sends position-removed at once. The app dims an Avatar after 2 minutes and removes it at 10.
No AI model is used in Iteration 1. The rules below are the baselines that a model is measured against in Iteration 2 (section 4.4).
Matching. A request names an event, a size from 2 to 4 and the User's hashtags. It is refused when the event has started, the User already holds a Shared Quest for it, or a request is already waiting. In each round:
- The main server says which requests still stand; the rest expire.
- For each event and size, the earliest request is placed into the earliest Open Quest of that event with the same capacity and a free place, never the requester's own.
- The remaining requests of one event and size are grouped greedily: start from the earliest and add the candidate who shares the most hashtags (the earlier one on a tie) until the size is reached.
- For each group the main server is asked, every round until it answers, to create a Closed Quest of that capacity led by the earliest requester.
Shuttle vehicles. The operator reports vehicles as positions on its own drawing of the route. Every 15 seconds on weekdays from 08:00 to 21:00 the worker posts them. The main server places each vehicle at the nearest stop of the drawing, keeps the set in Redis for 60 seconds and pushes it to every app. The app glides the marker along the route line to the new stop over 10 seconds, and drops a vehicle that has been silent for 60 seconds.
Campus Boundary and the Place at a point. The boundary polygon (the OpenStreetMap relation plus 10 m) is tested in server code on every upload; a position outside is not kept, and the answer says so. The same code returns the Place whose outline contains a point (within 5 m) or is near it (within 20 m).
Publishing a collected notice. A notice is published automatically only when a body line labelled 일시, 일자, 일정 or 기간 gives a start with an unambiguous time of day, and the place names exactly one Place. Everything else is a Draft with the original text and a link to the source.
Class Quests. Computed when the Quest list is read: one Quest per class that meets today (Asia/Seoul), in the shape of a stored Quest. Nothing is stored, and every changing route refuses them.
- Caching. No cache layer. Reference data is loaded from seed files at start, the app's query cache is invalidated by signals, and walking routes are never cached (Kakao's terms).
- Upload limits. A position is uploaded every 5 seconds in front and every 30 seconds in the background, under a foreground-service notification. An upload is refused when the switch is off, the position is older than 60 seconds or more than 10 seconds ahead, or its accuracy is worse than 100 m (all provisional).
- Concurrency. Capacity is checked inside the transaction under a row lock. Both Users are locked in id order for Invite Links and Friend requests. A refresh token reused within 60 seconds counts as a retry; later it ends the session.
- Repeated requests. The NestJS idempotency module with a Redis store. Records are scoped to the User and kept 24 hours; a request that is still running answers 409, and the same key with a different body answers 422.
-
Own code or a library. Own native map modules around the Kakao SDKs, because no maintained wrapper exists. A library for Google sign-in and
google-auth-libraryfor tokens, because security code is not hand-written. Own parsers and own geometry. - Security. Sharing is off by default, and each permission is explained before it is asked. Sign-out ends the session, revokes the tokens and clears the position. Secrets between servers have 32 or more characters and are compared in constant time; the worker and match server listen on the loopback interface only. The admin token is an HttpOnly cookie. Left for deployment: Redis and database passwords, rate limits and CORS.
Modules follow the features, which is also how the work is divided:
| Project | Modules |
|---|---|
| main-server |
auth, users, administrators, friends, invite-links, meetups, quests, parties, location-sharing, matching, timetable, global-events, places, menus, shuttle, walking-route, collection, common, demo
|
| socket-server |
users, signals, shuttle
|
| worker-server |
menu, event, shuttle
|
| match-server | matching |
| Mobile app |
screens, features, api, live, position, map, design-system
|
The goal was a runnable Android, iOS prototype in which a student goes from a campus event to companions and sees them on the map, meets a Friend, and uses the campus services. It was built on a clean branch between 2026-09-28 and 2026-10-08.
| Attempted | Result |
|---|---|
| Four NestJS servers, two databases, Redis and Docker Compose, with lint, format, type checks and CI from the first commit | Worked. One command starts the system. |
| SNU-only Google sign-in with sessions and renewal | Worked on the server and from a build installed on a physical phone. |
| Collecting the event list, three menu pages and the shuttle operator | Worked: 82 posts (13 published, 69 Drafts), 626 menu lines over 7 days, vehicles observed in service hours. |
| Places with outlines and the Campus Boundary from open data | Worked: 230 Places, 216 with outlines. |
| Quests, Parties, Friends, Invite Links, Meetups, Matching and Location Sharing | Worked on the servers and in the flow tests. The Quest and Party model was reworked mid-iteration (section 4.2). |
| The map through the team's own Kakao modules | Worked on a physical Android phone and iPhone. |
| Every wireframe screen that a server route serves | Worked on a physical Android phone and iPhone. |
| Background position sending under a notification | Built; not verified on a phone. |
| The admin site, and flow tests that stand in for every external source | Worked against the real servers. |
Not reached:
- Checks that need a person are open: an Invite Link opened from a messenger, the background notification, and the shuttle line compared with a ride.
- Private Events, Private Zones, blocking and the AI chat were not built; their controls answer "준비 중이에요".
| Earlier plan (0.1) | Now | Why |
|---|---|---|
| Request and response over Redis between all servers | HTTP with secrets for one-to-one calls; Redis events only to the socket servers | One receiver, an answer and a timeout; the worker and match server need no Redis. |
| Recruiting, capacity and Join Policy on the Party | On the Quest; the Party is the group together now | Recruiting happens days ahead; sharing happens only while together. |
| Matching the moment enough requests exist, with an AI sentence per match | Rounds with placement first, then grouping; no sentence | An Open Quest with a free place is a better match; the sentence added no gradeable value. |
| Buildings from OpenStreetMap with generated ids | Places from the campus map with ids from the source; outlines from the national map | Ids must survive reloads; OpenStreetMap lacked outlines. |
| Dishes with prices | Menu lines as written | Only 114 of 303 lines were a dish with a price. |
| A strict Campus Boundary; sessions enforced at the next renewal | A 10 m margin; a sessions table read on every request | A phone's error put Users at the gate outside; sign-out must take effect at once. |
| Friends by Invite Link only; one time per class | Friend ID and requests added; 1 to 10 times per class | A code typed in person is the common case; real timetables meet twice a week. |
- Servers. A socket opened with the token of an ended session stays until the token expires, at most an hour. Redis and PostgreSQL run without passwords until the hardening of Iteration 4. Collected posts are never read again, so a change at the source is missed. Vehicles are placed at stops only, and 14 Places have no outline. A tunnel address changes, while Invite Links and App Links need a fixed one.
- App. Background sharing and App Links are Android-only, and the map needs an ARM device. Friend states ("공강", "수업 중") and event announcers are still sample data. The Android 13 notification permission is not requested, and the legal texts are placeholders.
The Iteration 1 review (2026-10-07) found the prototype beyond expectations and the technical depth lacking: one main feature must hold a hard problem, ideally solved with AI; "AI-based Matching" is a weak key metric; Party and Quest confuse; and it must be clear whether the product is about real Friends or matched strangers. The direction below answers it and is confirmed at the Iteration 2 kickoff.
| Planned for Iteration 2 | Approach |
|---|---|
| Two words on screen. The Quest is shown as "약속" and the Party as "파티", the 약속 while it runs. A Meetup is a proposed 약속. | A rename of the screens and pages; the server models stay. Friends come first, and the stranger path stays on the roadmap until beta data shows demand. |
| The AI core: the Campus Reader. One module turns a Korean notice into typed fields (times, a Place or "none", a confidence that publishes, drafts or discards) and a timetable screenshot into classes with rooms. | A vision-capable model with constrained JSON output behind a provider interface like Grouping, then deterministic validation and Place linking that may abstain. Measured as per-field precision and recall against the rules of section 3.6 on the same labelled set. |
| The technical depth: the campus mobility model. Free windows from the timetables, a slope-aware walking graph of the campus, and a planner that proposes a Place and a time window every member can reach before their next class. | A self-hosted routing engine (Valhalla on OpenStreetMap with elevation) behind a WalkingTimes interface, and a planner over members, Places and 5-minute slots that minimises the longest walk. Measured as walking-time error on held-out walks. |
| Evaluation before the model. 100 notices and 30 screenshots labelled twice, with a held-out split frozen before any prompt work. |
pnpm eval:events and pnpm eval:walks, which a grader can run again. The Iteration 3 AI evaluation plan grows from this. |
| Notifications and workflow. Push notifications, the demo APK built in CI, and a demo video on a real phone every iteration. | A notification is the first message that must not be lost, so an outbox or a durable queue is decided then (decision 3). |
Open decisions:
- Whether the glossary term stays "Quest" with "약속" as its screen word. The code and its tests use the term.
- Which department seeds the beta, and whether the stranger path is built this semester.
- Which model provider runs the Campus Reader. Structured output and no training on user images are required.
- A fixed https host for Invite Links and App Links, which the cloud deployment of Iteration 4 gives.
- The provisional numbers (60-second rounds, 5 and 30 second uploads, the upload limits, the Collection times), each measured when its feature is tested on phones.
Deferred rather than dropped: the AI chat, Matching with AI, Private Events from a poster, organizer tools and study spaces.
The patterns below are already applied in the code. Two of them are documented in detail in Iteration 5.
-
Strategy.
Groupingis an abstract class with one provider,HashtagGrouping, inmatch-server/src/matching/. The Matching round depends on the abstract class only, so a model-based grouping replaces the rule in one module.GoogleIdTokenVerifierin the main server has the same shape, withGoogleAuthLibraryVerifieras its provider. -
Adapter.
Mapinmobile/src/map/is the one map interface that screens use.native-map.tsxand theSnuNowMapnative module adapt the Kakao Maps SDK to it, andplain-map.tsxstands in where the SDK is missing, so a different map provider changes nothing in the screens. -
Observer (publish and subscribe).
SignalsServicein the main server publishes a signal without knowing who listens.SignalsGatewayin the socket server delivers it to the Users' rooms, and the listeners inmobile/src/live/fetch again the lists that the signal names. -
Idempotency key. Requests that create a Quest, a step, a Meetup, a class or an event carry an
Idempotency-Keyfrom the app, and a handler marked@Idempotent()runs once for each key.RedisIdempotencyStoreinmain-server/src/common/keeps the records behind theIdempotencyStoreinterface of the NestJS module, each step as one atomic Redis script, so a request that is sent twice never creates a second Quest or Meetup. -
Singleton, through dependency injection.
PrismaService, the Redis connection andSignalsServicein the main server are each created once by the NestJS container and injected where they are needed. No class builds its own dependencies, which is what lets a provider be swapped in one module.
SNU Now
Documents
AI Collaboration Reports
Logs