Download - Reactive Model-View-ViewModel Architecture
![Page 1: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/1.jpg)
Reactive Model-View-ViewModel모바일응용프로그램아키텍쳐
이규원
https://www.facebook.com/gyuwon.yi
@styletigger
![Page 2: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/2.jpg)
MVVMModel-View-ViewModel
![Page 3: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/3.jpg)
MVVM(Model-View-ViewModel)
• In 2005, John Gossman(Microsoft)
• Separation of Presentation and Presentation Logic
• Two-way Binding
• Properties and Commands
• Testability
• WPF, Silverlight, Xamarin Forms, AngularJS, EmberJS, KnockoutJS, RoboBinding
• Introduction to Model/View/ViewModel pattern for building WPF apps
• WPF Apps With The Model-View-ViewModel Design Pattern
![Page 4: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/4.jpg)
ViewModel
Presentation Logic
View
User Interface
Model
Business Logic and Data
2-way Binding
MVVM 응용프로그램
![Page 5: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/5.jpg)
ViewModel
Presentation Logic
View
User Interface
API
Service
2-way Binding
Model
Server Client
서비스클라이언트응용프로그램
![Page 6: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/6.jpg)
상태동기화State Synchronization
![Page 7: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/7.jpg)
Showcase …
Showcase 15
no envy
Showcase …
…
… …
Showcase 15
no envy
Profile
Showcase 15
no envy
Showcase …
…
New Feed Explore Notifications
단일컨텐트에대한다중뷰
![Page 8: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/8.jpg)
Showcase …
Showcase 15
1 envy
Showcase …
…
… …
Showcase 15
1 envy
New Feed Explore Notifications
Profile
Showcase 15
1 envy
Showcase …
…
뷰상태동기화
![Page 9: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/9.jpg)
동기화흐름Flow of Synchronization
![Page 10: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/10.jpg)
Showcase …
Showcase 15
1 envy
Showcase …
…
… …
Showcase 15
1 envy
New Feed Explore Notifications
Profile
Showcase 15
1 envy
Showcase …
…
뷰모델사이의데이터흐름
![Page 11: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/11.jpg)
VM
VM VM
VM VM
VM
뷰모델사이의데이터흐름
![Page 12: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/12.jpg)
![Page 13: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/13.jpg)
![Page 14: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/14.jpg)
ViewModelRepository
One-way
Push
Publish
One-way
단방향데이터흐름
![Page 15: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/15.jpg)
Scale
Complexity
상태동기화복잡도
![Page 16: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/16.jpg)
불변모델개체Immutable Model Objects
![Page 17: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/17.jpg)
ShowcaseViewModel
IsEnvied: bool
EnvyCount: int
뷰모델속성을통한데이터노출
![Page 18: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/18.jpg)
<Button BackgroundColor="{Binding IsEnvied, Converter={...}}" />
<Label Text="{Binding EnvyCount, StringFormat='...'}" />
뷰모델속성바인딩
![Page 19: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/19.jpg)
IsEnvied EnvyCount PropertyChanged Validity
False 0 Valid
True 0 "IsEnvied" Invalid
True 1 "EnvyCount" Valid
뷰모델속성상태변경
![Page 20: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/20.jpg)
뷰에바인딩된속성에대한 PropertyChanged 이벤트는레이아웃계산과그리기작업을촉발한다
![Page 21: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/21.jpg)
모델속성을통한데이터노출
ShowcaseViewModel ShowcaseModel
IsEnvied: bool
EnvyCount: int
Model
![Page 22: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/22.jpg)
<Button BackgroundColor="{Binding Model.IsEnvied, Converter={...}}" />
<Label Text="{Binding Model.EnvyCount, StringFormat='...'}" />
모델속성바인딩
![Page 23: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/23.jpg)
Model PropertyChanged Validity
{ IsEnvied: False, EnvyCount: 0 } Valid
{ IsEnvied: True, EnvyCount: 1 } "Model" Valid
모델상태변경
![Page 24: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/24.jpg)
단, 모델개체는불변성을가져야한다
![Page 25: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/25.jpg)
Immutable Objects
• Cannot be modified after initialization
• Thread-safe
• Readability
• Runtime efficiency
• Functional programming
![Page 26: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/26.jpg)
모델속성을통한데이터노출
ShowcaseViewModel ShowcaseModel<< immutable >>
IsEnvied: bool
EnvyCount: int
Model
![Page 27: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/27.jpg)
불변모델반응형스트림Reactive Streams of Immutable Model Objects
![Page 28: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/28.jpg)
데이터흐름
•컨텐트상태는불변성을가지는모델개체로캡슐화
•뷰모델은모델개체를속성으로뷰에노출
•컨텐트상태가변경되면새로운모델인스턴스발행
![Page 29: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/29.jpg)
‘불변성을가진모델개체의반응형스트림’
![Page 30: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/30.jpg)
![Page 31: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/31.jpg)
…Model
Revision n + 2Model
Revision n + 1Model
Revision n…
![Page 32: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/32.jpg)
스트림저장소
![Page 33: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/33.jpg)
Rx(Reactive Extensions)
• Observables + LINQ + Schedulers
• Asynchronous and event-based programming
• C#, JavaScript, C, C++, Ruby, Python, Java
• Microsoft Open Tech(*RxJava by Netflix)
• https://rx.codeplex.com/
![Page 34: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/34.jpg)
반응형스트림
public interface IObservable<out T>
{
IDisposable Subscribe(IObserver<T> observer);
}
public interface IObserver<in T>
{
void OnCompleted();
void OnError(Exception error);
void OnNext(T value);
}
![Page 35: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/35.jpg)
모델
public abstract class Model<TModel, TId>
where TModel : Model<TModel, TId>
where TId : IEquatable<TId>
{
private readonly TId _id;
public TId Id { get { return _id; } }
protected Model(TId id)
{
_id = id;
}
}
![Page 36: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/36.jpg)
모델
public sealed class ShowcaseModel : Model<ShowcaseModel, long>
{
private readonly bool _isEnvied;
private readonly int _envyCount;
public ShowcaseModel(long id, bool isEnvied, int envyCount) : base(id)
{
_isEnvied = isEnvied;
_envyCount = envyCount;
}
public bool IsEnvied { get { return _isEnvied; } }
public int EnvyCount { get { return _envyCount; } }
}
![Page 37: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/37.jpg)
스트림저장소
public static class StreamStore<TModel, TId>
where TModel : Model<TModel, TId>
where TId: IEquatable<TId>
{
public static IObservable<TModel> GetStream(TId id);
public static void Push(TModel model);
}
![Page 38: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/38.jpg)
모델-뷰모델
public abstract class ModelViewModel<TModel, TId> : ViewModel
{
private TModel _model = null;
public TModel Model
{
get { return _model;}
set { SetProperty(ref _model, value); }
}
protected virtual void OnNext(TModel next)
{
Model = next;
}
}
![Page 39: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/39.jpg)
스트림구독Stream Subscription
![Page 40: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/40.jpg)
![Page 41: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/41.jpg)
약한스트림구독
ModelViewModel
Observer WeakSubscription
Stream
Reference
Weak Reference
![Page 42: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/42.jpg)
약한스트림구독
public abstract class ModelViewModel<TModel, TId> : ViewModel
{
private readonly TId _id;
private readonly IObserver<TModel> _observer;
private readonly IDisposable _subscription;
protected ModelViewModel(TId id)
{
_id = id;
var stream = StreamStore<TModel, TId>.GetStream(id);
_observer = Observer.Create<TModel>(onNext: OnNext);
_subscription = new WeakSubscription<TModel>(stream, _observer);
}
~ModelViewModel() { _subscription.Dispose(); }
}
![Page 43: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/43.jpg)
스위치연산자Switch Operator
![Page 44: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/44.jpg)
비동기데이터조회
•응용프로그램반응성향상
• Futures Pattern
![Page 45: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/45.jpg)
중복된비동기데이터조회
•불필요한무효화(invalidation)
•시작과종료순서의불일치
![Page 46: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/46.jpg)
![Page 47: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/47.jpg)
Switch()
“Transforms an observable sequence of observable sequences into an observable sequence producing values only from the most recent observable sequence.”
- from MSDN
![Page 48: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/48.jpg)
스위치
![Page 49: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/49.jpg)
스위치
public abstract class ModelViewModel<TModel, TId> : ViewModel
{
private readonly Subject<IObservable<TModel>> _spout = new Subject<IObservable<TModel>>();
protected ModelViewModel(TId id)
{
_spout.Switch()
.Subscribe(next => StreamStore<TModel, TId>.Push(next));
}
protected virtual void Push(IObservable<TModel> next)
{
_spout.OnNext(next);
}
}
![Page 50: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/50.jpg)
Task to Observable
public abstract class ModelViewModel<TModel, TId> : ViewModel
{
protected void Push(Task<TModel> next)
{
Push(next.ToObservable());
}
protected void Push(Func<Task<TModel>> next)
{
Push(next.Invoke().ToObservable());
}
}
![Page 51: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/51.jpg)
Model to Observable
public abstract class ModelViewModel<TModel, TId> : ViewModel
{
protected void Push(TModel next)
{
Push(Task.FromResult(next).ToObservable());
}
}
![Page 52: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/52.jpg)
병합연산자Coalescing Operator
![Page 53: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/53.jpg)
UserModel
Id : "user1"
UserName
ProfilePhotoUri
UserModel
Id: "user1"
UserName
ProfilePhotoUri
FolloweeCount
FollowerCount
Followees: 10
Followers: 24
목록조회와상세정보조회
![Page 54: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/54.jpg)
데이터유실
UserModel
Id : "user1"
UserName
ProfilePhotoUri
UserModel
Id: "user1"
UserName
ProfilePhotoUri
FolloweeCount
FollowerCount
Followees: 10
Followers: 24
UserModel
Id: "user1"
UserName
ProfilePhotoUri
FolloweeCount
FollowerCount
Followees: 10
Followers: 24
Push User Detail Page Push User List Page Pop
![Page 55: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/55.jpg)
![Page 56: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/56.jpg)
Id: "user1" UserName ProfilePhotoUri FolloweeCount FollowerCount
Id: "user1" UserName ProfilePhotoUri FolloweeCount FollowerCount
Id: "user1" UserName ProfilePhotoUri FolloweeCount FollowerCount
Coalesce
=
병합연산
![Page 57: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/57.jpg)
병합연산
public static class ModelExtensions
{
public static UserModel Coalesce(this UserModel user, UserModel other)
{
if (user == null) throw new ArgumentNullException("user");
if (other == null) return user;
if (other.Id != user.Id) throw new ArgumentException();
if (user.Equals(other)) return user;
return new UserModel(user.Id,
user.UserName,
user.ProfilePhotoUri,
user.FolloweeCount ?? other.FolloweeCount,
user.FollowerCount ?? other.FollowerCount);
}
}
![Page 58: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/58.jpg)
병합연산
public class UserViewModel : ModelViewModel<UserModel, string>
{
protected override void OnNext(UserModel next)
{
var current = Model;
base.OnNext(next.Coalesce(current));
}
}
![Page 59: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/59.jpg)
같음확인Equality Comparisons
![Page 60: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/60.jpg)
뷰모델속성설정
public class ViewModel : INotifyPropertyChanged
{
protected bool SetProperty<T>(ref T field,
T value,
[CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
NotifyPropertyChanged(propertyName);
return true;
}
}
![Page 61: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/61.jpg)
Changed!Changed…?
ShowcaseId: 15
IsEnvied: trueEnvyCount: 10
ShowcaseId: 15
IsEnvied: trueEnvyCount: 10
ShowcaseId: 15
IsEnvied: falseEnvyCount: 9
Changed…?
개체참조비교
![Page 62: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/62.jpg)
같음확인논리
public sealed class ShowcaseModel : Model<ShowcaseModel, long>, IEquatable<ShowcaseModel>
{
public bool Equals(ShowcaseModel other)
{
if (other == null)
return false;
if (object.ReferenceEquals(this, other))
return true;
return Id == other.Id &&
_isEnvied == other._isEnvied &&
_envyCount == other._envyCount;
}
}
![Page 63: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/63.jpg)
Changed!Changed…?
ShowcaseId: 15
IsEnvied: trueEnvyCount: 10
ShowcaseId: 15
IsEnvied: trueEnvyCount: 10
ShowcaseId: 15
IsEnvied: falseEnvyCount: 9
Not Changed
모델데이터비교
![Page 64: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/64.jpg)
정리
![Page 65: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/65.jpg)
Reactive MVVM Architecture
•단방향데이터흐름
•불변모델
•반응형스트림
•약한스트림구독
•스위치연산
•병합연산
•같음확인
![Page 66: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/66.jpg)
Reactive MVVM Architecture
ViewModel Model<< 1-way binding >>
Model<< push >>
Co
alescin
g
Equ
ality C
om
pariso
n
Stream Store
Switch
We
ak Su
bscrip
tion
View
Commands,Entry Fields
and UI States
Model<< publish >>
![Page 67: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/67.jpg)
![Page 68: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/68.jpg)
Reactive MVVM 아키텍쳐를적용한안드로이드앱을함께개발하실멋쟁이
프로그래머느님을애타게찾고있습니다.
public async Task<IEnvicase> CreateAndroidAsync(){
var team = await JoinUsAsync(new You());return team.CreateAndroidApp();
}
https://www.facebook.com/gyuwon.yi@styletigger
![Page 69: Reactive Model-View-ViewModel Architecture](https://reader033.vdocuments.pub/reader033/viewer/2022052509/55a522e91a28aba8348b4894/html5/thumbnails/69.jpg)
감사합니다