# Friendly Map 가족·친구·직장 관계를 한 앱에서. 새 친구가 "어디서 봤지?" 싶을 때 목적별 명함을 주고받아 관계를 유추한다. 안드로이드 우선, Flutter. 모든 기기가 서버 겸 클라이언트(P2P)이고, 외부 DB는 암호화된 임시 우편함일 뿐이다. ## 원칙 (1순위) 1. 구조가 기능보다 먼저다. 화면과 내부를 분리한다. 2. 모듈은 언제든 빼고 끼울 수 있다. 기능을 더하거나 빼도 다른 모듈을 고치지 않는다. 3. 파일은 300~400줄, 최대 700줄. 4. 모듈별로 시험한다. 시험이 끝난 모듈은 다시 만들지 않고 그 위에 쌓는다. 5. 한 번에 한 플랫폼. 지금은 안드로이드. 다른 플랫폼은 `platform_<이름>` 모듈만 새로 붙인다. ## 모듈과 의존 방향 (`tool/check_modules.py` 가 검사) | 모듈 | 하는 일 | 쓸 수 있는 모듈 | |---|---|---| | core | 모듈끼리 주고받는 약속(키·전송·암호·봉투·명함 항목 이름) | 없음 | | platform | 기기마다 다른 기능의 약속(보안 저장소, 저장 공간, 센서, QR, 발견, 위치, 통화 기록·전화, 알림, 주소록) | 없음 | | platform_android | platform 의 안드로이드 구현(플러그인은 여기서만) | core, platform, storage | | storage | 저장소 약속 + 메모리/SQLite 구현 | platform | | keys | 12단어 복구 코드, 키 파생, 둘만 아는 키 암호, 복구 조각 나누기·합치기 | core, platform | | sharing | 명함·대화방이 누구와 공유되는지 | storage | | sync | 봉투 만들기·반영, 충돌 규칙, 시계 맞추기, 재전송 | core, storage | | transport | 메모리·직접 연결·외부 보관함·늦게 만드는 연결 | core | | discovery | QR 내용, 흔들기 감지, 같은 네트워크 주소록 | platform | | inference | "어디서 봤지?" 유추와 추천 | core | | visibility | 공개 범위 표(정보 × 관계)와 명함 거르기. 순수 계산 | core | | layout | 홈·명함 틀(템플릿) 순서/보이기/편집 | core | | kinship | 가계도로 촌수·호칭 계산(순수) | 없음 | | events | 생일·기일 일정, 알림 날짜 계획, 통화 기록 요약(순수) | 없음 | | groups | 그룹 정의(이름·종류·순서·구성원), 비밀번호 잠금, 여러 번 틀리면 기다리기(순수) | core | | app | 서비스 묶음(`FriendlyApp`). 화면이 쓰는 유일한 내부 입구 | 위 모듈들(키·전송 구현 제외) | | ui | 화면. `app` 의 서비스만 쓴다 | app, core, platform, storage, discovery, inference, layout, visibility, kinship, events, groups | 모듈 폴더 밖의 `lib/main.dart`, `lib/composition/` 이 부품을 고르는 유일한 곳이다. 암호 구현·저장소·연결 방식을 바꾸려면 `lib/composition/android_bootstrap.dart` 의 해당 줄만 바꾼다. ## 시험 ``` tool/test_module.sh # 모듈 목록 tool/test_module.sh sync # 모듈 하나만 flutter test # 전체 python3 tool/check_modules.py # 줄 수·import 방향 ``` ## 시작 ``` flutter create --platforms=android --org com.friendlymap --project-name friendly_map . python3 tool/android_patch.py # 권한, 평문 통신, minSdk 23 flutter pub get flutter analyze flutter test flutter run ``` 보관함 서버(선택): `server/README.md`. 서버 주소를 설정 `mailbox_url` 에 넣으면 같은 네트워크 밖에서도 주고받는다. ## 알려진 한계 - 보관함의 우편함 토큰은 번호별 처음 쓴 기기에 묶인다(번호 선점 가능). 나중에 문자 인증으로 막는다. - 그룹 대화방은 사람이 나가도 키를 바꾸지 않는다. - 위치는 지도 타일 없이 레이더(내 위치 중심, 방위·거리)로 보여 준다. 지도 SDK는 붙이지 않았다. - 문자 인증은 만들어 뒀지만 문자 발송 주소(`--sms-webhook`)를 정해 서버에 넣어야 켜진다. 켜기 전에는 처음 쓴 기기에 묶이는 방식(번호 선점 가능)이다. ## 가족 모임 (촌수 · 통화 요약 · 생일/기일 알림) - 홈의 "가족 모임"에서 가족을 한 분씩 추가한다. 번호가 없는 친척도 넣을 수 있다(이 기기 안에서만 쓰이고 친구 요청은 가지 않는다). - "나의 부모/자녀/배우자"를 고르거나 부모·자녀·배우자를 이어 가계도를 만들면, 나와 내 자녀 각각의 눈으로 본 호칭과 촌수가 나온다(예: 내게는 친사촌 형 4촌, 내 아들에게는 당숙 5촌). - 사람을 누르면 관계, 통화 횟수·시간·마지막 통화·통화 잘 되는 시간대, 주제 메모, 다가오는 생일/기일을 요약하고 전화·문자·대화 버튼이 있다. - 돌아가신 날을 적으면 맨 위에 "故" 띠가 뜨고(설정에서 끌 수 있다) 생일 대신 기일을 알린다. - 설정 > "생일·기일 알림"에 `7, 2, 1, 0` 처럼 적으면 그 날짜 전 오전 9시에 알림이 울린다(기본 1일 전·당일). - 음력: 생일·기일마다 "음력"을 켤 수 있다(1951~2049년 변환표 내장, 기일은 윤달도 지원). 제사 알림이 음력 날짜로 잡힌다. - 한계: 통화 "내용"은 기기에 남지 않으므로 주제는 직접 적는다. 통화 기록 읽기(READ_CALL_LOG)는 Play 스토어 심사 제한이 있다. - 안드로이드: `call_log`, `url_launcher`, `flutter_local_notifications`, `timezone` 플러그인과 권한·수신기·desugaring 설정이 필요하다(`tool/android_patch.py` 가 맞춘다). ## 복구 조각 (친구에게 맡기기) - 설정 → 친구에게 맡기기: 12단어를 n조각으로 나눠(필요한 수 k) 친구마다 한 장씩 암호화해 보낸다. 친구 한 명의 조각으로는 아무것도 알 수 없다. - 친구는 설정 → 내가 맡은 조각에서 조각 글자를 본다. 새 폰에서 복구할 때 필요한 수만큼의 조각 글자를 "복구 코드로 되살리기" 칸에 한 줄씩 붙여 넣는다. - 조각 수나 필요한 수를 바꾸면 새로 나눠야 하고, 이전에 맡긴 조각은 새 조각과 섞이지 않는다. ## 공개 범위 - 내 정보 → 공개 범위 표(학교 · 회사·직책 · 생일 · 연락처 × 가족 · 회사 · 동문 · 그룹)에서 칸을 눌러 공개/일부/숨김을 정한다. - 명함을 보내거나 "다시 보내기"를 할 때 받는 사람의 관계(연결된 관계 + 보내는 명함 종류)에 맞게 걸러서 보낸다. 관계를 모르는 사람에게는 내가 고른 그대로 간다. - 표를 고쳐도 이미 보낸 명함은 "다시 보내기" 전까지 그대로다. ## 위치 공유 - 홈의 "위치" 구역에서 켠다. 기본은 꺼짐. - 켜면 1분마다, 공개 범위 표의 "실시간 위치"가 공개·일부인 관계의 친구에게만 보낸다. 관계를 모르는 친구에게는 보내지 않는다. "일부"는 약 1km 단위로 뭉개서 보낸다. - 위치는 둘만 아는 키로 잠가서 보내고, 받은 쪽에는 마지막 위치 하나만 남는다. - 안드로이드: `geolocator` 플러그인과 위치 권한이 필요하다 (`tool/android_patch.py` 가 권한을 넣는다). ## 번호 확인 (문자 인증) - 내 정보 → "내 번호 확인하기": 문자로 온 6자리를 넣으면 이 기기의 토큰이 내 번호에 묶인다. 폰을 바꾼 진짜 주인은 다시 확인하면 새 기기로 넘어오고, 이전 기기는 막힌다. - 서버: `python3 mailbox_server.py --sms-webhook <주소> --require-verify`. 문자 발송은 `SmsSender` 하나(웹훅은 `{to,text}` 를 POST)라 업체를 바꿔도 서버 나머지는 그대로다. - `--require-verify` 를 켜면 확인을 마친 기기만 우편함을 열 수 있다 (번호 선점 차단). 켜지 않으면 예전처럼 처음 쓴 기기에 묶인다. - 5분 만료, 번호당 1시간 3번, 5번 오답이면 새로 요청. `--sms-log` 는 시험용(문자를 보내지 않고 기록)이라 실서비스에서 쓰면 안 된다. - 서버 시험: `cd server && python3 -m unittest test_mailbox test_verify` ## 그룹 · 주소록 가져오기 - 설정(내 정보 → 저장·보안) → "그룹 · 주소록". 처음 그룹은 가족·학교·회사·그룹·여행이고, 더하기·이름과 종류 고치기·순서 바꾸기·지우기가 된다(지워도 사람은 인맥에 남는다). - 그룹마다 비밀번호(4~16자)를 걸 수 있다. 잠긴 그룹은 열 때 비밀번호를 묻고, 닫혀 있으면 인맥 목록에서도 그 사람들을 숨긴다. 연 뒤 정한 시간(기본 5분, 1~60분)이 지나거나 앱을 내리면 다시 잠근다. - 5번 틀리면 30초, 그 뒤로 틀릴 때마다 두 배(최대 1시간) 기다려야 한다. 비밀번호는 PBKDF2-SHA256 값만 남긴다. 잊으면 그 그룹은 지우고 다시 만들어야 한다. - "주소록에서 가져오기": 이름·초성(ㄱㅁㅅ)·번호로 찾고, 휴대폰 주소록 그룹(라벨)으로 거르고, 이미 있는 사람은 빼고 볼 수 있다. 한 명씩 또는 보이는 사람을 모두 고른 뒤, 넣을 그룹(여러 개)을 골라 가져온다. - 번호가 여럿이면 휴대폰(010) 번호를 먼저 쓴다. 가져온 사람은 이 기기 인맥에만 들어가고, "친구 요청도 보내기"를 켰을 때만 상대에게 요청이 간다. 주소록은 읽기만 한다. - 그룹은 이 기기에만 저장한다. 안드로이드: `flutter_contacts` 플러그인과 READ_CONTACTS 권한이 필요하다(`tool/android_patch.py` 가 넣는다).