forked from baron/baron-sso
RP 이동 및 연동 해지 기능 로직 문서화
This commit is contained in:
138
docs/rp_redirection_implementation.md
Normal file
138
docs/rp_redirection_implementation.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# 연동된 RP 홈페이지 이동 기능 구현 가이드
|
||||||
|
|
||||||
|
## 1. 개요
|
||||||
|
UserFront 대시보드의 '활동상황' 섹션에서 연동된 RP(Relying Party) 카드를 클릭했을 때, 해당 서비스의 홈페이지로 이동하는 기능의 구현 상세입니다.
|
||||||
|
특히, RP 설정에 홈페이지 주소(`client_uri`)가 명시되지 않은 경우에도 `Redirect URI`를 기반으로 주소를 추론하여 이동할 수 있도록 **Fallback 로직**이 적용되었습니다.
|
||||||
|
|
||||||
|
## 2. 동작 흐름 (Data Flow)
|
||||||
|
|
||||||
|
1. **초기 로딩**: 사용자가 대시보드에 접속하면 프론트엔드는 백엔드에 연동된 RP 목록을 요청합니다.
|
||||||
|
2. **데이터 가공 (Backend)**:
|
||||||
|
* 백엔드는 Ory Hydra에서 사용자의 동의(Consent) 세션을 조회합니다.
|
||||||
|
* 각 RP(Client) 정보에서 `client_uri`를 확인합니다.
|
||||||
|
* **Fallback**: 만약 `client_uri`가 비어있다면, `redirect_uris`의 첫 번째 주소를 파싱하여 `Scheme`과 `Host` (예: `https://gitea.hmac.kr`)를 추출해 홈페이지 주소로 사용합니다.
|
||||||
|
3. **렌더링 (Frontend)**:
|
||||||
|
* 응답받은 목록을 기반으로 카드를 생성합니다.
|
||||||
|
* RP 상태가 '활성(active)'인 경우에만 클릭 이벤트를 활성화합니다.
|
||||||
|
4. **사용자 인터랙션**:
|
||||||
|
* 사용자가 카드를 클릭하면 `url_launcher`를 통해 새 브라우저 탭에서 해당 주소를 엽니다.
|
||||||
|
* 주소가 없는 경우 사용자에게 안내 메시지(SnackBar)를 표시합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 백엔드 구현 상세 (Go)
|
||||||
|
|
||||||
|
### 파일: `backend/internal/handler/auth_handler.go`
|
||||||
|
|
||||||
|
#### 3.1 구조체 변경
|
||||||
|
API 응답 모델인 `linkedRpSummary`에 `URL` 필드를 추가하여 프론트엔드로 전달할 수 있게 했습니다.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type linkedRpSummary struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Logo string `json:"logo,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"` // 추가된 필드
|
||||||
|
LastAuthenticatedAt string `json:"lastAuthenticatedAt,omitempty"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Scopes []string `json:"scopes,omitempty"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2 URL 할당 및 Fallback 로직 (`ListLinkedRps`)
|
||||||
|
Hydra Client 정보 매핑 시, `ClientURI` 부재 시 `RedirectURIs`를 활용하는 로직이 핵심입니다.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ClientURI가 없으면 RedirectURIs에서 호스트 부분만 추출하여 URL로 사용 (Fallback)
|
||||||
|
clientURL := strings.TrimSpace(client.ClientURI)
|
||||||
|
if clientURL == "" && len(client.RedirectURIs) > 0 {
|
||||||
|
// 예: https://gitea.hmac.kr/callback -> https://gitea.hmac.kr
|
||||||
|
if parsed, err := url.Parse(client.RedirectURIs[0]); err == nil {
|
||||||
|
clientURL = fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
records[clientID] = &linkedRpRecord{
|
||||||
|
linkedRpSummary: linkedRpSummary{
|
||||||
|
// ...
|
||||||
|
URL: clientURL, // 가공된 URL 할당
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 프론트엔드 구현 상세 (Flutter)
|
||||||
|
|
||||||
|
### 파일: `userfront/lib/features/dashboard/presentation/dashboard_screen.dart`
|
||||||
|
|
||||||
|
#### 4.1 모델 업데이트
|
||||||
|
백엔드 응답을 처리하기 위해 `LinkedRp` 모델에 `url` 필드를 추가했습니다.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class LinkedRp {
|
||||||
|
final String id;
|
||||||
|
// ...
|
||||||
|
final String url; // 추가된 필드
|
||||||
|
|
||||||
|
factory LinkedRp.fromJson(Map<String, dynamic> json) {
|
||||||
|
return LinkedRp(
|
||||||
|
// ...
|
||||||
|
url: json['url']?.toString() ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2 UI 인터랙션 구현 (`_buildActivityCard`)
|
||||||
|
카드의 클릭 가능 여부를 판단하고, 클릭 시 이동 로직을 처리합니다.
|
||||||
|
|
||||||
|
1. **클릭 조건**: RP 상태가 '활성'(`isActive`)이면 클릭 가능하도록 설정합니다.
|
||||||
|
2. **시각적 피드백**:
|
||||||
|
* 활성 상태인 경우 테두리 색상(Green)과 그림자 효과(BoxShadow)를 적용하여 클릭 가능함을 암시합니다.
|
||||||
|
* `MouseRegion`을 사용하여 마우스 오버 시 포인터 커서(`SystemMouseCursors.click`)를 표시합니다.
|
||||||
|
3. **이동 로직**:
|
||||||
|
* `url_launcher` 패키지의 `launchUrl`을 사용합니다.
|
||||||
|
* URL이 비어있거나 유효하지 않은 경우 `ScaffoldMessenger`를 통해 안내 메시지를 띄웁니다.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// 활성 상태면 클릭 가능
|
||||||
|
final isClickable = isActive;
|
||||||
|
|
||||||
|
// ... (UI 스타일링 코드 생략)
|
||||||
|
|
||||||
|
if (isClickable) {
|
||||||
|
return MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
if (item.url != null && item.url!.isNotEmpty) {
|
||||||
|
final uri = Uri.parse(item.url!);
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri);
|
||||||
|
} else {
|
||||||
|
// 브라우저 실행 실패 시
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('해당 링크를 열 수 없습니다.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// URL 정보가 없는 경우 (백엔드 Fallback 실패 등)
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('이동할 페이지 주소(Client URI)가 설정되지 않았습니다.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: opaqueCard,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 요약
|
||||||
|
이 기능은 사용자가 별도의 설정 없이도 연동된 서비스로 쉽게 이동할 수 있도록 편의성을 제공합니다. 백엔드에서의 **지능적인 주소 추론**과 프론트엔드에서의 **직관적인 UI 피드백**이 결합되어 완성되었습니다.
|
||||||
Reference in New Issue
Block a user